From 499b24362cf7797428d2fc3c172243ff38913e64 Mon Sep 17 00:00:00 2001 From: "stainless-sdks[bot]" <167585319+stainless-sdks[bot]@users.noreply.github.com> Date: Thu, 11 Dec 2025 22:20:42 +0000 Subject: [PATCH 01/88] Initial commit --- README.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 00000000..d6d34668 --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +# stagehand-python \ No newline at end of file From f0c6b9539a9d76a9ec5efda2dc779d507b80e5ef Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 11 Dec 2025 22:21:01 +0000 Subject: [PATCH 02/88] initial commit --- .devcontainer/Dockerfile | 9 + .devcontainer/devcontainer.json | 43 + .github/workflows/ci.yml | 98 + .gitignore | 15 + .python-version | 1 + .stats.yml | 4 + .vscode/settings.json | 3 + Brewfile | 2 + CONTRIBUTING.md | 128 ++ LICENSE | 201 ++ README.md | 404 +++- SECURITY.md | 23 + api.md | 27 + bin/publish-pypi | 6 + examples/.keep | 4 + noxfile.py | 9 + pyproject.toml | 269 +++ requirements-dev.lock | 149 ++ requirements.lock | 76 + scripts/bootstrap | 27 + scripts/format | 8 + scripts/lint | 11 + scripts/mock | 41 + scripts/test | 61 + scripts/utils/ruffen-docs.py | 167 ++ scripts/utils/upload-artifact.sh | 27 + src/stagehand/__init__.py | 104 + src/stagehand/_base_client.py | 1995 +++++++++++++++++ src/stagehand/_client.py | 480 ++++ src/stagehand/_compat.py | 219 ++ src/stagehand/_constants.py | 14 + src/stagehand/_exceptions.py | 108 + src/stagehand/_files.py | 123 + src/stagehand/_models.py | 857 +++++++ src/stagehand/_qs.py | 150 ++ src/stagehand/_resource.py | 43 + src/stagehand/_response.py | 830 +++++++ src/stagehand/_streaming.py | 333 +++ src/stagehand/_types.py | 261 +++ src/stagehand/_utils/__init__.py | 64 + src/stagehand/_utils/_compat.py | 45 + src/stagehand/_utils/_datetime_parse.py | 136 ++ src/stagehand/_utils/_logs.py | 25 + src/stagehand/_utils/_proxy.py | 65 + src/stagehand/_utils/_reflection.py | 42 + src/stagehand/_utils/_resources_proxy.py | 24 + src/stagehand/_utils/_streams.py | 12 + src/stagehand/_utils/_sync.py | 58 + src/stagehand/_utils/_transform.py | 457 ++++ src/stagehand/_utils/_typing.py | 156 ++ src/stagehand/_utils/_utils.py | 421 ++++ src/stagehand/_version.py | 4 + src/stagehand/lib/.keep | 4 + src/stagehand/py.typed | 0 src/stagehand/resources/__init__.py | 19 + src/stagehand/resources/sessions.py | 963 ++++++++ src/stagehand/types/__init__.py | 20 + src/stagehand/types/action.py | 26 + src/stagehand/types/action_param.py | 27 + src/stagehand/types/model_config_param.py | 22 + src/stagehand/types/session_act_params.py | 37 + src/stagehand/types/session_act_response.py | 19 + src/stagehand/types/session_end_response.py | 11 + .../types/session_execute_agent_params.py | 46 + .../types/session_execute_agent_response.py | 15 + src/stagehand/types/session_extract_params.py | 35 + .../types/session_extract_response.py | 17 + .../types/session_navigate_params.py | 25 + .../types/session_navigate_response.py | 17 + src/stagehand/types/session_observe_params.py | 31 + .../types/session_observe_response.py | 10 + src/stagehand/types/session_start_params.py | 44 + src/stagehand/types/session_start_response.py | 15 + tests/__init__.py | 1 + tests/api_resources/__init__.py | 1 + tests/api_resources/test_sessions.py | 862 +++++++ tests/conftest.py | 84 + tests/sample_file.txt | 1 + tests/test_client.py | 1765 +++++++++++++++ tests/test_deepcopy.py | 58 + tests/test_extract_files.py | 64 + tests/test_files.py | 51 + tests/test_models.py | 963 ++++++++ tests/test_qs.py | 78 + tests/test_required_args.py | 111 + tests/test_response.py | 277 +++ tests/test_streaming.py | 248 ++ tests/test_transform.py | 460 ++++ tests/test_utils/test_datetime_parse.py | 110 + tests/test_utils/test_proxy.py | 34 + tests/test_utils/test_typing.py | 73 + tests/utils.py | 167 ++ 92 files changed, 15649 insertions(+), 1 deletion(-) create mode 100644 .devcontainer/Dockerfile create mode 100644 .devcontainer/devcontainer.json create mode 100644 .github/workflows/ci.yml create mode 100644 .gitignore create mode 100644 .python-version create mode 100644 .stats.yml create mode 100644 .vscode/settings.json create mode 100644 Brewfile create mode 100644 CONTRIBUTING.md create mode 100644 LICENSE create mode 100644 SECURITY.md create mode 100644 api.md create mode 100644 bin/publish-pypi create mode 100644 examples/.keep create mode 100644 noxfile.py create mode 100644 pyproject.toml create mode 100644 requirements-dev.lock create mode 100644 requirements.lock create mode 100755 scripts/bootstrap create mode 100755 scripts/format create mode 100755 scripts/lint create mode 100755 scripts/mock create mode 100755 scripts/test create mode 100644 scripts/utils/ruffen-docs.py create mode 100755 scripts/utils/upload-artifact.sh create mode 100644 src/stagehand/__init__.py create mode 100644 src/stagehand/_base_client.py create mode 100644 src/stagehand/_client.py create mode 100644 src/stagehand/_compat.py create mode 100644 src/stagehand/_constants.py create mode 100644 src/stagehand/_exceptions.py create mode 100644 src/stagehand/_files.py create mode 100644 src/stagehand/_models.py create mode 100644 src/stagehand/_qs.py create mode 100644 src/stagehand/_resource.py create mode 100644 src/stagehand/_response.py create mode 100644 src/stagehand/_streaming.py create mode 100644 src/stagehand/_types.py create mode 100644 src/stagehand/_utils/__init__.py create mode 100644 src/stagehand/_utils/_compat.py create mode 100644 src/stagehand/_utils/_datetime_parse.py create mode 100644 src/stagehand/_utils/_logs.py create mode 100644 src/stagehand/_utils/_proxy.py create mode 100644 src/stagehand/_utils/_reflection.py create mode 100644 src/stagehand/_utils/_resources_proxy.py create mode 100644 src/stagehand/_utils/_streams.py create mode 100644 src/stagehand/_utils/_sync.py create mode 100644 src/stagehand/_utils/_transform.py create mode 100644 src/stagehand/_utils/_typing.py create mode 100644 src/stagehand/_utils/_utils.py create mode 100644 src/stagehand/_version.py create mode 100644 src/stagehand/lib/.keep create mode 100644 src/stagehand/py.typed create mode 100644 src/stagehand/resources/__init__.py create mode 100644 src/stagehand/resources/sessions.py create mode 100644 src/stagehand/types/__init__.py create mode 100644 src/stagehand/types/action.py create mode 100644 src/stagehand/types/action_param.py create mode 100644 src/stagehand/types/model_config_param.py create mode 100644 src/stagehand/types/session_act_params.py create mode 100644 src/stagehand/types/session_act_response.py create mode 100644 src/stagehand/types/session_end_response.py create mode 100644 src/stagehand/types/session_execute_agent_params.py create mode 100644 src/stagehand/types/session_execute_agent_response.py create mode 100644 src/stagehand/types/session_extract_params.py create mode 100644 src/stagehand/types/session_extract_response.py create mode 100644 src/stagehand/types/session_navigate_params.py create mode 100644 src/stagehand/types/session_navigate_response.py create mode 100644 src/stagehand/types/session_observe_params.py create mode 100644 src/stagehand/types/session_observe_response.py create mode 100644 src/stagehand/types/session_start_params.py create mode 100644 src/stagehand/types/session_start_response.py create mode 100644 tests/__init__.py create mode 100644 tests/api_resources/__init__.py create mode 100644 tests/api_resources/test_sessions.py create mode 100644 tests/conftest.py create mode 100644 tests/sample_file.txt create mode 100644 tests/test_client.py create mode 100644 tests/test_deepcopy.py create mode 100644 tests/test_extract_files.py create mode 100644 tests/test_files.py create mode 100644 tests/test_models.py create mode 100644 tests/test_qs.py create mode 100644 tests/test_required_args.py create mode 100644 tests/test_response.py create mode 100644 tests/test_streaming.py create mode 100644 tests/test_transform.py create mode 100644 tests/test_utils/test_datetime_parse.py create mode 100644 tests/test_utils/test_proxy.py create mode 100644 tests/test_utils/test_typing.py create mode 100644 tests/utils.py diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 00000000..ff261bad --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,9 @@ +ARG VARIANT="3.9" +FROM mcr.microsoft.com/vscode/devcontainers/python:0-${VARIANT} + +USER vscode + +RUN curl -sSf https://rye.astral.sh/get | RYE_VERSION="0.44.0" RYE_INSTALL_OPTION="--yes" bash +ENV PATH=/home/vscode/.rye/shims:$PATH + +RUN echo "[[ -d .venv ]] && source .venv/bin/activate || export PATH=\$PATH" >> /home/vscode/.bashrc diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 00000000..c17fdc16 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,43 @@ +// For format details, see https://aka.ms/devcontainer.json. For config options, see the +// README at: https://github.com/devcontainers/templates/tree/main/src/debian +{ + "name": "Debian", + "build": { + "dockerfile": "Dockerfile", + "context": ".." + }, + + "postStartCommand": "rye sync --all-features", + + "customizations": { + "vscode": { + "extensions": [ + "ms-python.python" + ], + "settings": { + "terminal.integrated.shell.linux": "/bin/bash", + "python.pythonPath": ".venv/bin/python", + "python.defaultInterpreterPath": ".venv/bin/python", + "python.typeChecking": "basic", + "terminal.integrated.env.linux": { + "PATH": "/home/vscode/.rye/shims:${env:PATH}" + } + } + } + }, + "features": { + "ghcr.io/devcontainers/features/node:1": {} + } + + // Features to add to the dev container. More info: https://containers.dev/features. + // "features": {}, + + // Use 'forwardPorts' to make a list of ports inside the container available locally. + // "forwardPorts": [], + + // Configure tool-specific properties. + // "customizations": {}, + + // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. + // "remoteUser": "root" +} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..edab221c --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,98 @@ +name: CI +on: + push: + branches-ignore: + - 'generated' + - 'codegen/**' + - 'integrated/**' + - 'stl-preview-head/**' + - 'stl-preview-base/**' + pull_request: + branches-ignore: + - 'stl-preview-head/**' + - 'stl-preview-base/**' + +jobs: + lint: + timeout-minutes: 10 + name: lint + runs-on: ${{ github.repository == 'stainless-sdks/stagehand-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} + if: github.event_name == 'push' || github.event.pull_request.head.repo.fork + steps: + - uses: actions/checkout@v4 + + - name: Install Rye + run: | + curl -sSf https://rye.astral.sh/get | bash + echo "$HOME/.rye/shims" >> $GITHUB_PATH + env: + RYE_VERSION: '0.44.0' + RYE_INSTALL_OPTION: '--yes' + + - name: Install dependencies + run: rye sync --all-features + + - name: Run lints + run: ./scripts/lint + + build: + if: github.event_name == 'push' || github.event.pull_request.head.repo.fork + timeout-minutes: 10 + name: build + permissions: + contents: read + id-token: write + runs-on: ${{ github.repository == 'stainless-sdks/stagehand-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} + steps: + - uses: actions/checkout@v4 + + - name: Install Rye + run: | + curl -sSf https://rye.astral.sh/get | bash + echo "$HOME/.rye/shims" >> $GITHUB_PATH + env: + RYE_VERSION: '0.44.0' + RYE_INSTALL_OPTION: '--yes' + + - name: Install dependencies + run: rye sync --all-features + + - name: Run build + run: rye build + + - name: Get GitHub OIDC Token + if: github.repository == 'stainless-sdks/stagehand-python' + id: github-oidc + uses: actions/github-script@v6 + with: + script: core.setOutput('github_token', await core.getIDToken()); + + - name: Upload tarball + if: github.repository == 'stainless-sdks/stagehand-python' + env: + URL: https://pkg.stainless.com/s + AUTH: ${{ steps.github-oidc.outputs.github_token }} + SHA: ${{ github.sha }} + run: ./scripts/utils/upload-artifact.sh + + test: + timeout-minutes: 10 + name: test + runs-on: ${{ github.repository == 'stainless-sdks/stagehand-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} + if: github.event_name == 'push' || github.event.pull_request.head.repo.fork + steps: + - uses: actions/checkout@v4 + + - name: Install Rye + run: | + curl -sSf https://rye.astral.sh/get | bash + echo "$HOME/.rye/shims" >> $GITHUB_PATH + env: + RYE_VERSION: '0.44.0' + RYE_INSTALL_OPTION: '--yes' + + - name: Bootstrap + run: ./scripts/bootstrap + + - name: Run tests + run: ./scripts/test diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..95ceb189 --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ +.prism.log +_dev + +__pycache__ +.mypy_cache + +dist + +.venv +.idea + +.env +.envrc +codegen.log +Brewfile.lock.json diff --git a/.python-version b/.python-version new file mode 100644 index 00000000..43077b24 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.9.18 diff --git a/.stats.yml b/.stats.yml new file mode 100644 index 00000000..bc4543c7 --- /dev/null +++ b/.stats.yml @@ -0,0 +1,4 @@ +configured_endpoints: 7 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fstagehand-0c12f985340be2a9287e8e01ff8733f7f2d02e019149d1ae95f1a8f8798c6690.yml +openapi_spec_hash: efb79934e1dc63763dd4e8493b825273 +config_hash: 03e0374dee89691195d5b8472e6ecc82 diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..5b010307 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "python.analysis.importFormat": "relative", +} diff --git a/Brewfile b/Brewfile new file mode 100644 index 00000000..492ca37b --- /dev/null +++ b/Brewfile @@ -0,0 +1,2 @@ +brew "rye" + diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..10d07c5d --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,128 @@ +## Setting up the environment + +### With Rye + +We use [Rye](https://rye.astral.sh/) to manage dependencies because it will automatically provision a Python environment with the expected Python version. To set it up, run: + +```sh +$ ./scripts/bootstrap +``` + +Or [install Rye manually](https://rye.astral.sh/guide/installation/) and run: + +```sh +$ rye sync --all-features +``` + +You can then run scripts using `rye run python script.py` or by activating the virtual environment: + +```sh +# Activate the virtual environment - https://docs.python.org/3/library/venv.html#how-venvs-work +$ source .venv/bin/activate + +# now you can omit the `rye run` prefix +$ python script.py +``` + +### Without Rye + +Alternatively if you don't want to install `Rye`, you can stick with the standard `pip` setup by ensuring you have the Python version specified in `.python-version`, create a virtual environment however you desire and then install dependencies using this command: + +```sh +$ pip install -r requirements-dev.lock +``` + +## Modifying/Adding code + +Most of the SDK is generated code. Modifications to code will be persisted between generations, but may +result in merge conflicts between manual patches and changes from the generator. The generator will never +modify the contents of the `src/stagehand/lib/` and `examples/` directories. + +## Adding and running examples + +All files in the `examples/` directory are not modified by the generator and can be freely edited or added to. + +```py +# add an example to examples/.py + +#!/usr/bin/env -S rye run python +… +``` + +```sh +$ chmod +x examples/.py +# run the example against your api +$ ./examples/.py +``` + +## Using the repository from source + +If you’d like to use the repository from source, you can either install from git or link to a cloned repository: + +To install via git: + +```sh +$ pip install git+ssh://git@github.com/stainless-sdks/stagehand-python.git +``` + +Alternatively, you can build from source and install the wheel file: + +Building this package will create two files in the `dist/` directory, a `.tar.gz` containing the source files and a `.whl` that can be used to install the package efficiently. + +To create a distributable version of the library, all you have to do is run this command: + +```sh +$ rye build +# or +$ python -m build +``` + +Then to install: + +```sh +$ pip install ./path-to-wheel-file.whl +``` + +## Running tests + +Most tests require you to [set up a mock server](https://github.com/stoplightio/prism) against the OpenAPI spec to run the tests. + +```sh +# you will need npm installed +$ npx prism mock path/to/your/openapi.yml +``` + +```sh +$ ./scripts/test +``` + +## Linting and formatting + +This repository uses [ruff](https://github.com/astral-sh/ruff) and +[black](https://github.com/psf/black) to format the code in the repository. + +To lint: + +```sh +$ ./scripts/lint +``` + +To format and fix all ruff issues automatically: + +```sh +$ ./scripts/format +``` + +## Publishing and releases + +Changes made to this repository via the automated release PR pipeline should publish to PyPI automatically. If +the changes aren't made through the automated pipeline, you may want to make releases manually. + +### Publish with a GitHub workflow + +You can release to package managers by using [the `Publish PyPI` GitHub action](https://www.github.com/stainless-sdks/stagehand-python/actions/workflows/publish-pypi.yml). This requires a setup organization or repository secret to be set up. + +### Publish manually + +If you need to manually release a package, you can run the `bin/publish-pypi` script with a `PYPI_TOKEN` set on +the environment. diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..6b24314a --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2025 Stagehand + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md index d6d34668..1588e9e6 100644 --- a/README.md +++ b/README.md @@ -1 +1,403 @@ -# stagehand-python \ No newline at end of file +# Stagehand Python API library + + +[![PyPI version](https://img.shields.io/pypi/v/stagehand.svg?label=pypi%20(stable))](https://pypi.org/project/stagehand/) + +The Stagehand Python library provides convenient access to the Stagehand REST API from any Python 3.9+ +application. The library includes type definitions for all request params and response fields, +and offers both synchronous and asynchronous clients powered by [httpx](https://github.com/encode/httpx). + +It is generated with [Stainless](https://www.stainless.com/). + +## Documentation + +The REST API documentation can be found on [browserbase.com](https://browserbase.com). The full API of this library can be found in [api.md](api.md). + +## Installation + +```sh +# install from this staging repo +pip install git+ssh://git@github.com/stainless-sdks/stagehand-python.git +``` + +> [!NOTE] +> Once this package is [published to PyPI](https://www.stainless.com/docs/guides/publish), this will become: `pip install stagehand` + +## Usage + +The full API of this library can be found in [api.md](api.md). + +```python +import os +from stagehand import Stagehand + +client = Stagehand( + api_key=os.environ.get("STAGEHAND_API_KEY"), # This is the default and can be omitted + # defaults to "production". + environment="environment_1", +) + +response = client.sessions.start( + env="LOCAL", +) +print(response.available) +``` + +While you can provide an `api_key` keyword argument, +we recommend using [python-dotenv](https://pypi.org/project/python-dotenv/) +to add `STAGEHAND_API_KEY="My API Key"` to your `.env` file +so that your API Key is not stored in source control. + +## Async usage + +Simply import `AsyncStagehand` instead of `Stagehand` and use `await` with each API call: + +```python +import os +import asyncio +from stagehand import AsyncStagehand + +client = AsyncStagehand( + api_key=os.environ.get("STAGEHAND_API_KEY"), # This is the default and can be omitted + # defaults to "production". + environment="environment_1", +) + + +async def main() -> None: + response = await client.sessions.start( + env="LOCAL", + ) + print(response.available) + + +asyncio.run(main()) +``` + +Functionality between the synchronous and asynchronous clients is otherwise identical. + +### With aiohttp + +By default, the async client uses `httpx` for HTTP requests. However, for improved concurrency performance you may also use `aiohttp` as the HTTP backend. + +You can enable this by installing `aiohttp`: + +```sh +# install from this staging repo +pip install 'stagehand[aiohttp] @ git+ssh://git@github.com/stainless-sdks/stagehand-python.git' +``` + +Then you can enable it by instantiating the client with `http_client=DefaultAioHttpClient()`: + +```python +import os +import asyncio +from stagehand import DefaultAioHttpClient +from stagehand import AsyncStagehand + + +async def main() -> None: + async with AsyncStagehand( + api_key=os.environ.get("STAGEHAND_API_KEY"), # This is the default and can be omitted + http_client=DefaultAioHttpClient(), + ) as client: + response = await client.sessions.start( + env="LOCAL", + ) + print(response.available) + + +asyncio.run(main()) +``` + +## Using types + +Nested request parameters are [TypedDicts](https://docs.python.org/3/library/typing.html#typing.TypedDict). Responses are [Pydantic models](https://docs.pydantic.dev) which also provide helper methods for things like: + +- Serializing back into JSON, `model.to_json()` +- Converting to a dictionary, `model.to_dict()` + +Typed requests and responses provide autocomplete and documentation within your editor. If you would like to see type errors in VS Code to help catch bugs earlier, set `python.analysis.typeCheckingMode` to `basic`. + +## Nested params + +Nested parameters are dictionaries, typed using `TypedDict`, for example: + +```python +from stagehand import Stagehand + +client = Stagehand() + +response = client.sessions.start( + env="LOCAL", + local_browser_launch_options={}, +) +print(response.local_browser_launch_options) +``` + +## Handling errors + +When the library is unable to connect to the API (for example, due to network connection problems or a timeout), a subclass of `stagehand.APIConnectionError` is raised. + +When the API returns a non-success status code (that is, 4xx or 5xx +response), a subclass of `stagehand.APIStatusError` is raised, containing `status_code` and `response` properties. + +All errors inherit from `stagehand.APIError`. + +```python +import stagehand +from stagehand import Stagehand + +client = Stagehand() + +try: + client.sessions.start( + env="LOCAL", + ) +except stagehand.APIConnectionError as e: + print("The server could not be reached") + print(e.__cause__) # an underlying Exception, likely raised within httpx. +except stagehand.RateLimitError as e: + print("A 429 status code was received; we should back off a bit.") +except stagehand.APIStatusError as e: + print("Another non-200-range status code was received") + print(e.status_code) + print(e.response) +``` + +Error codes are as follows: + +| Status Code | Error Type | +| ----------- | -------------------------- | +| 400 | `BadRequestError` | +| 401 | `AuthenticationError` | +| 403 | `PermissionDeniedError` | +| 404 | `NotFoundError` | +| 422 | `UnprocessableEntityError` | +| 429 | `RateLimitError` | +| >=500 | `InternalServerError` | +| N/A | `APIConnectionError` | + +### Retries + +Certain errors are automatically retried 2 times by default, with a short exponential backoff. +Connection errors (for example, due to a network connectivity problem), 408 Request Timeout, 409 Conflict, +429 Rate Limit, and >=500 Internal errors are all retried by default. + +You can use the `max_retries` option to configure or disable retry settings: + +```python +from stagehand import Stagehand + +# Configure the default for all requests: +client = Stagehand( + # default is 2 + max_retries=0, +) + +# Or, configure per-request: +client.with_options(max_retries=5).sessions.start( + env="LOCAL", +) +``` + +### Timeouts + +By default requests time out after 1 minute. You can configure this with a `timeout` option, +which accepts a float or an [`httpx.Timeout`](https://www.python-httpx.org/advanced/timeouts/#fine-tuning-the-configuration) object: + +```python +from stagehand import Stagehand + +# Configure the default for all requests: +client = Stagehand( + # 20 seconds (default is 1 minute) + timeout=20.0, +) + +# More granular control: +client = Stagehand( + timeout=httpx.Timeout(60.0, read=5.0, write=10.0, connect=2.0), +) + +# Override per-request: +client.with_options(timeout=5.0).sessions.start( + env="LOCAL", +) +``` + +On timeout, an `APITimeoutError` is thrown. + +Note that requests that time out are [retried twice by default](#retries). + +## Advanced + +### Logging + +We use the standard library [`logging`](https://docs.python.org/3/library/logging.html) module. + +You can enable logging by setting the environment variable `STAGEHAND_LOG` to `info`. + +```shell +$ export STAGEHAND_LOG=info +``` + +Or to `debug` for more verbose logging. + +### How to tell whether `None` means `null` or missing + +In an API response, a field may be explicitly `null`, or missing entirely; in either case, its value is `None` in this library. You can differentiate the two cases with `.model_fields_set`: + +```py +if response.my_field is None: + if 'my_field' not in response.model_fields_set: + print('Got json like {}, without a "my_field" key present at all.') + else: + print('Got json like {"my_field": null}.') +``` + +### Accessing raw response data (e.g. headers) + +The "raw" Response object can be accessed by prefixing `.with_raw_response.` to any HTTP method call, e.g., + +```py +from stagehand import Stagehand + +client = Stagehand() +response = client.sessions.with_raw_response.start( + env="LOCAL", +) +print(response.headers.get('X-My-Header')) + +session = response.parse() # get the object that `sessions.start()` would have returned +print(session.available) +``` + +These methods return an [`APIResponse`](https://github.com/stainless-sdks/stagehand-python/tree/main/src/stagehand/_response.py) object. + +The async client returns an [`AsyncAPIResponse`](https://github.com/stainless-sdks/stagehand-python/tree/main/src/stagehand/_response.py) with the same structure, the only difference being `await`able methods for reading the response content. + +#### `.with_streaming_response` + +The above interface eagerly reads the full response body when you make the request, which may not always be what you want. + +To stream the response body, use `.with_streaming_response` instead, which requires a context manager and only reads the response body once you call `.read()`, `.text()`, `.json()`, `.iter_bytes()`, `.iter_text()`, `.iter_lines()` or `.parse()`. In the async client, these are async methods. + +```python +with client.sessions.with_streaming_response.start( + env="LOCAL", +) as response: + print(response.headers.get("X-My-Header")) + + for line in response.iter_lines(): + print(line) +``` + +The context manager is required so that the response will reliably be closed. + +### Making custom/undocumented requests + +This library is typed for convenient access to the documented API. + +If you need to access undocumented endpoints, params, or response properties, the library can still be used. + +#### Undocumented endpoints + +To make requests to undocumented endpoints, you can make requests using `client.get`, `client.post`, and other +http verbs. Options on the client will be respected (such as retries) when making this request. + +```py +import httpx + +response = client.post( + "/foo", + cast_to=httpx.Response, + body={"my_param": True}, +) + +print(response.headers.get("x-foo")) +``` + +#### Undocumented request params + +If you want to explicitly send an extra param, you can do so with the `extra_query`, `extra_body`, and `extra_headers` request +options. + +#### Undocumented response properties + +To access undocumented response properties, you can access the extra fields like `response.unknown_prop`. You +can also get all the extra fields on the Pydantic model as a dict with +[`response.model_extra`](https://docs.pydantic.dev/latest/api/base_model/#pydantic.BaseModel.model_extra). + +### Configuring the HTTP client + +You can directly override the [httpx client](https://www.python-httpx.org/api/#client) to customize it for your use case, including: + +- Support for [proxies](https://www.python-httpx.org/advanced/proxies/) +- Custom [transports](https://www.python-httpx.org/advanced/transports/) +- Additional [advanced](https://www.python-httpx.org/advanced/clients/) functionality + +```python +import httpx +from stagehand import Stagehand, DefaultHttpxClient + +client = Stagehand( + # Or use the `STAGEHAND_BASE_URL` env var + base_url="http://my.test.server.example.com:8083", + http_client=DefaultHttpxClient( + proxy="http://my.test.proxy.example.com", + transport=httpx.HTTPTransport(local_address="0.0.0.0"), + ), +) +``` + +You can also customize the client on a per-request basis by using `with_options()`: + +```python +client.with_options(http_client=DefaultHttpxClient(...)) +``` + +### Managing HTTP resources + +By default the library closes underlying HTTP connections whenever the client is [garbage collected](https://docs.python.org/3/reference/datamodel.html#object.__del__). You can manually close the client using the `.close()` method if desired, or with a context manager that closes when exiting. + +```py +from stagehand import Stagehand + +with Stagehand() as client: + # make requests here + ... + +# HTTP client is now closed +``` + +## Versioning + +This package generally follows [SemVer](https://semver.org/spec/v2.0.0.html) conventions, though certain backwards-incompatible changes may be released as minor versions: + +1. Changes that only affect static types, without breaking runtime behavior. +2. Changes to library internals which are technically public but not intended or documented for external use. _(Please open a GitHub issue to let us know if you are relying on such internals.)_ +3. Changes that we do not expect to impact the vast majority of users in practice. + +We take backwards-compatibility seriously and work hard to ensure you can rely on a smooth upgrade experience. + +We are keen for your feedback; please open an [issue](https://www.github.com/stainless-sdks/stagehand-python/issues) with questions, bugs, or suggestions. + +### Determining the installed version + +If you've upgraded to the latest version but aren't seeing any new features you were expecting then your python environment is likely still using an older version. + +You can determine the version that is being used at runtime with: + +```py +import stagehand +print(stagehand.__version__) +``` + +## Requirements + +Python 3.9 or higher. + +## Contributing + +See [the contributing documentation](./CONTRIBUTING.md). diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 00000000..dcfc419a --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,23 @@ +# Security Policy + +## Reporting Security Issues + +This SDK is generated by [Stainless Software Inc](http://stainless.com). Stainless takes security seriously, and encourages you to report any security vulnerability promptly so that appropriate action can be taken. + +To report a security issue, please contact the Stainless team at security@stainless.com. + +## Responsible Disclosure + +We appreciate the efforts of security researchers and individuals who help us maintain the security of +SDKs we generate. If you believe you have found a security vulnerability, please adhere to responsible +disclosure practices by allowing us a reasonable amount of time to investigate and address the issue +before making any information public. + +## Reporting Non-SDK Related Security Issues + +If you encounter security issues that are not directly related to SDKs but pertain to the services +or products provided by Stagehand, please follow the respective company's security reporting guidelines. + +--- + +Thank you for helping us keep the SDKs and systems they interact with secure. diff --git a/api.md b/api.md new file mode 100644 index 00000000..6eb90647 --- /dev/null +++ b/api.md @@ -0,0 +1,27 @@ +# Sessions + +Types: + +```python +from stagehand.types import ( + Action, + ModelConfig, + SessionActResponse, + SessionEndResponse, + SessionExecuteAgentResponse, + SessionExtractResponse, + SessionNavigateResponse, + SessionObserveResponse, + SessionStartResponse, +) +``` + +Methods: + +- client.sessions.act(session_id, \*\*params) -> SessionActResponse +- client.sessions.end(session_id) -> SessionEndResponse +- client.sessions.execute_agent(session_id, \*\*params) -> SessionExecuteAgentResponse +- client.sessions.extract(session_id, \*\*params) -> SessionExtractResponse +- client.sessions.navigate(session_id, \*\*params) -> Optional[SessionNavigateResponse] +- client.sessions.observe(session_id, \*\*params) -> SessionObserveResponse +- client.sessions.start(\*\*params) -> SessionStartResponse diff --git a/bin/publish-pypi b/bin/publish-pypi new file mode 100644 index 00000000..826054e9 --- /dev/null +++ b/bin/publish-pypi @@ -0,0 +1,6 @@ +#!/usr/bin/env bash + +set -eux +mkdir -p dist +rye build --clean +rye publish --yes --token=$PYPI_TOKEN diff --git a/examples/.keep b/examples/.keep new file mode 100644 index 00000000..d8c73e93 --- /dev/null +++ b/examples/.keep @@ -0,0 +1,4 @@ +File generated from our OpenAPI spec by Stainless. + +This directory can be used to store example files demonstrating usage of this SDK. +It is ignored by Stainless code generation and its content (other than this keep file) won't be touched. \ No newline at end of file diff --git a/noxfile.py b/noxfile.py new file mode 100644 index 00000000..53bca7ff --- /dev/null +++ b/noxfile.py @@ -0,0 +1,9 @@ +import nox + + +@nox.session(reuse_venv=True, name="test-pydantic-v1") +def test_pydantic_v1(session: nox.Session) -> None: + session.install("-r", "requirements-dev.lock") + session.install("pydantic<2") + + session.run("pytest", "--showlocals", "--ignore=tests/functional", *session.posargs) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..aa099b61 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,269 @@ +[project] +name = "stagehand" +version = "0.0.1" +description = "The official Python library for the stagehand API" +dynamic = ["readme"] +license = "Apache-2.0" +authors = [ +{ name = "Stagehand", email = "" }, +] + +dependencies = [ + "httpx>=0.23.0, <1", + "pydantic>=1.9.0, <3", + "typing-extensions>=4.10, <5", + "anyio>=3.5.0, <5", + "distro>=1.7.0, <2", + "sniffio", +] + +requires-python = ">= 3.9" +classifiers = [ + "Typing :: Typed", + "Intended Audience :: Developers", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", + "Operating System :: OS Independent", + "Operating System :: POSIX", + "Operating System :: MacOS", + "Operating System :: POSIX :: Linux", + "Operating System :: Microsoft :: Windows", + "Topic :: Software Development :: Libraries :: Python Modules", + "License :: OSI Approved :: Apache Software License" +] + +[project.urls] +Homepage = "https://github.com/stainless-sdks/stagehand-python" +Repository = "https://github.com/stainless-sdks/stagehand-python" + +[project.optional-dependencies] +aiohttp = ["aiohttp", "httpx_aiohttp>=0.1.9"] + +[tool.rye] +managed = true +# version pins are in requirements-dev.lock +dev-dependencies = [ + "pyright==1.1.399", + "mypy==1.17", + "respx", + "pytest", + "pytest-asyncio", + "ruff", + "time-machine", + "nox", + "dirty-equals>=0.6.0", + "importlib-metadata>=6.7.0", + "rich>=13.7.1", + "pytest-xdist>=3.6.1", +] + +[tool.rye.scripts] +format = { chain = [ + "format:ruff", + "format:docs", + "fix:ruff", + # run formatting again to fix any inconsistencies when imports are stripped + "format:ruff", +]} +"format:docs" = "python scripts/utils/ruffen-docs.py README.md api.md" +"format:ruff" = "ruff format" + +"lint" = { chain = [ + "check:ruff", + "typecheck", + "check:importable", +]} +"check:ruff" = "ruff check ." +"fix:ruff" = "ruff check --fix ." + +"check:importable" = "python -c 'import stagehand'" + +typecheck = { chain = [ + "typecheck:pyright", + "typecheck:mypy" +]} +"typecheck:pyright" = "pyright" +"typecheck:verify-types" = "pyright --verifytypes stagehand --ignoreexternal" +"typecheck:mypy" = "mypy ." + +[build-system] +requires = ["hatchling==1.26.3", "hatch-fancy-pypi-readme"] +build-backend = "hatchling.build" + +[tool.hatch.build] +include = [ + "src/*" +] + +[tool.hatch.build.targets.wheel] +packages = ["src/stagehand"] + +[tool.hatch.build.targets.sdist] +# Basically everything except hidden files/directories (such as .github, .devcontainers, .python-version, etc) +include = [ + "/*.toml", + "/*.json", + "/*.lock", + "/*.md", + "/mypy.ini", + "/noxfile.py", + "bin/*", + "examples/*", + "src/*", + "tests/*", +] + +[tool.hatch.metadata.hooks.fancy-pypi-readme] +content-type = "text/markdown" + +[[tool.hatch.metadata.hooks.fancy-pypi-readme.fragments]] +path = "README.md" + +[[tool.hatch.metadata.hooks.fancy-pypi-readme.substitutions]] +# replace relative links with absolute links +pattern = '\[(.+?)\]\(((?!https?://)\S+?)\)' +replacement = '[\1](https://github.com/stainless-sdks/stagehand-python/tree/main/\g<2>)' + +[tool.pytest.ini_options] +testpaths = ["tests"] +addopts = "--tb=short -n auto" +xfail_strict = true +asyncio_mode = "auto" +asyncio_default_fixture_loop_scope = "session" +filterwarnings = [ + "error" +] + +[tool.pyright] +# this enables practically every flag given by pyright. +# there are a couple of flags that are still disabled by +# default in strict mode as they are experimental and niche. +typeCheckingMode = "strict" +pythonVersion = "3.9" + +exclude = [ + "_dev", + ".venv", + ".nox", + ".git", +] + +reportImplicitOverride = true +reportOverlappingOverload = false + +reportImportCycles = false +reportPrivateUsage = false + +[tool.mypy] +pretty = true +show_error_codes = true + +# Exclude _files.py because mypy isn't smart enough to apply +# the correct type narrowing and as this is an internal module +# it's fine to just use Pyright. +# +# We also exclude our `tests` as mypy doesn't always infer +# types correctly and Pyright will still catch any type errors. +exclude = ['src/stagehand/_files.py', '_dev/.*.py', 'tests/.*'] + +strict_equality = true +implicit_reexport = true +check_untyped_defs = true +no_implicit_optional = true + +warn_return_any = true +warn_unreachable = true +warn_unused_configs = true + +# Turn these options off as it could cause conflicts +# with the Pyright options. +warn_unused_ignores = false +warn_redundant_casts = false + +disallow_any_generics = true +disallow_untyped_defs = true +disallow_untyped_calls = true +disallow_subclassing_any = true +disallow_incomplete_defs = true +disallow_untyped_decorators = true +cache_fine_grained = true + +# By default, mypy reports an error if you assign a value to the result +# of a function call that doesn't return anything. We do this in our test +# cases: +# ``` +# result = ... +# assert result is None +# ``` +# Changing this codegen to make mypy happy would increase complexity +# and would not be worth it. +disable_error_code = "func-returns-value,overload-cannot-match" + +# https://github.com/python/mypy/issues/12162 +[[tool.mypy.overrides]] +module = "black.files.*" +ignore_errors = true +ignore_missing_imports = true + + +[tool.ruff] +line-length = 120 +output-format = "grouped" +target-version = "py38" + +[tool.ruff.format] +docstring-code-format = true + +[tool.ruff.lint] +select = [ + # isort + "I", + # bugbear rules + "B", + # remove unused imports + "F401", + # check for missing future annotations + "FA102", + # bare except statements + "E722", + # unused arguments + "ARG", + # print statements + "T201", + "T203", + # misuse of typing.TYPE_CHECKING + "TC004", + # import rules + "TID251", +] +ignore = [ + # mutable defaults + "B006", +] +unfixable = [ + # disable auto fix for print statements + "T201", + "T203", +] + +extend-safe-fixes = ["FA102"] + +[tool.ruff.lint.flake8-tidy-imports.banned-api] +"functools.lru_cache".msg = "This function does not retain type information for the wrapped function's arguments; The `lru_cache` function from `_utils` should be used instead" + +[tool.ruff.lint.isort] +length-sort = true +length-sort-straight = true +combine-as-imports = true +extra-standard-library = ["typing_extensions"] +known-first-party = ["stagehand", "tests"] + +[tool.ruff.lint.per-file-ignores] +"bin/**.py" = ["T201", "T203"] +"scripts/**.py" = ["T201", "T203"] +"tests/**.py" = ["T201", "T203"] +"examples/**.py" = ["T201", "T203"] diff --git a/requirements-dev.lock b/requirements-dev.lock new file mode 100644 index 00000000..5f6cc3bb --- /dev/null +++ b/requirements-dev.lock @@ -0,0 +1,149 @@ +# generated by rye +# use `rye lock` or `rye sync` to update this lockfile +# +# last locked with the following flags: +# pre: false +# features: [] +# all-features: true +# with-sources: false +# generate-hashes: false +# universal: false + +-e file:. +aiohappyeyeballs==2.6.1 + # via aiohttp +aiohttp==3.13.2 + # via httpx-aiohttp + # via stagehand +aiosignal==1.4.0 + # via aiohttp +annotated-types==0.7.0 + # via pydantic +anyio==4.12.0 + # via httpx + # via stagehand +argcomplete==3.6.3 + # via nox +async-timeout==5.0.1 + # via aiohttp +attrs==25.4.0 + # via aiohttp + # via nox +backports-asyncio-runner==1.2.0 + # via pytest-asyncio +certifi==2025.11.12 + # via httpcore + # via httpx +colorlog==6.10.1 + # via nox +dependency-groups==1.3.1 + # via nox +dirty-equals==0.11 +distlib==0.4.0 + # via virtualenv +distro==1.9.0 + # via stagehand +exceptiongroup==1.3.1 + # via anyio + # via pytest +execnet==2.1.2 + # via pytest-xdist +filelock==3.19.1 + # via virtualenv +frozenlist==1.8.0 + # via aiohttp + # via aiosignal +h11==0.16.0 + # via httpcore +httpcore==1.0.9 + # via httpx +httpx==0.28.1 + # via httpx-aiohttp + # via respx + # via stagehand +httpx-aiohttp==0.1.9 + # via stagehand +humanize==4.13.0 + # via nox +idna==3.11 + # via anyio + # via httpx + # via yarl +importlib-metadata==8.7.0 +iniconfig==2.1.0 + # via pytest +markdown-it-py==3.0.0 + # via rich +mdurl==0.1.2 + # via markdown-it-py +multidict==6.7.0 + # via aiohttp + # via yarl +mypy==1.17.0 +mypy-extensions==1.1.0 + # via mypy +nodeenv==1.9.1 + # via pyright +nox==2025.11.12 +packaging==25.0 + # via dependency-groups + # via nox + # via pytest +pathspec==0.12.1 + # via mypy +platformdirs==4.4.0 + # via virtualenv +pluggy==1.6.0 + # via pytest +propcache==0.4.1 + # via aiohttp + # via yarl +pydantic==2.12.5 + # via stagehand +pydantic-core==2.41.5 + # via pydantic +pygments==2.19.2 + # via pytest + # via rich +pyright==1.1.399 +pytest==8.4.2 + # via pytest-asyncio + # via pytest-xdist +pytest-asyncio==1.2.0 +pytest-xdist==3.8.0 +python-dateutil==2.9.0.post0 + # via time-machine +respx==0.22.0 +rich==14.2.0 +ruff==0.14.7 +six==1.17.0 + # via python-dateutil +sniffio==1.3.1 + # via stagehand +time-machine==2.19.0 +tomli==2.3.0 + # via dependency-groups + # via mypy + # via nox + # via pytest +typing-extensions==4.15.0 + # via aiosignal + # via anyio + # via exceptiongroup + # via multidict + # via mypy + # via pydantic + # via pydantic-core + # via pyright + # via pytest-asyncio + # via stagehand + # via typing-inspection + # via virtualenv +typing-inspection==0.4.2 + # via pydantic +virtualenv==20.35.4 + # via nox +yarl==1.22.0 + # via aiohttp +zipp==3.23.0 + # via importlib-metadata diff --git a/requirements.lock b/requirements.lock new file mode 100644 index 00000000..3a9ba327 --- /dev/null +++ b/requirements.lock @@ -0,0 +1,76 @@ +# generated by rye +# use `rye lock` or `rye sync` to update this lockfile +# +# last locked with the following flags: +# pre: false +# features: [] +# all-features: true +# with-sources: false +# generate-hashes: false +# universal: false + +-e file:. +aiohappyeyeballs==2.6.1 + # via aiohttp +aiohttp==3.13.2 + # via httpx-aiohttp + # via stagehand +aiosignal==1.4.0 + # via aiohttp +annotated-types==0.7.0 + # via pydantic +anyio==4.12.0 + # via httpx + # via stagehand +async-timeout==5.0.1 + # via aiohttp +attrs==25.4.0 + # via aiohttp +certifi==2025.11.12 + # via httpcore + # via httpx +distro==1.9.0 + # via stagehand +exceptiongroup==1.3.1 + # via anyio +frozenlist==1.8.0 + # via aiohttp + # via aiosignal +h11==0.16.0 + # via httpcore +httpcore==1.0.9 + # via httpx +httpx==0.28.1 + # via httpx-aiohttp + # via stagehand +httpx-aiohttp==0.1.9 + # via stagehand +idna==3.11 + # via anyio + # via httpx + # via yarl +multidict==6.7.0 + # via aiohttp + # via yarl +propcache==0.4.1 + # via aiohttp + # via yarl +pydantic==2.12.5 + # via stagehand +pydantic-core==2.41.5 + # via pydantic +sniffio==1.3.1 + # via stagehand +typing-extensions==4.15.0 + # via aiosignal + # via anyio + # via exceptiongroup + # via multidict + # via pydantic + # via pydantic-core + # via stagehand + # via typing-inspection +typing-inspection==0.4.2 + # via pydantic +yarl==1.22.0 + # via aiohttp diff --git a/scripts/bootstrap b/scripts/bootstrap new file mode 100755 index 00000000..b430fee3 --- /dev/null +++ b/scripts/bootstrap @@ -0,0 +1,27 @@ +#!/usr/bin/env bash + +set -e + +cd "$(dirname "$0")/.." + +if [ -f "Brewfile" ] && [ "$(uname -s)" = "Darwin" ] && [ "$SKIP_BREW" != "1" ] && [ -t 0 ]; then + brew bundle check >/dev/null 2>&1 || { + echo -n "==> Install Homebrew dependencies? (y/N): " + read -r response + case "$response" in + [yY][eE][sS]|[yY]) + brew bundle + ;; + *) + ;; + esac + echo + } +fi + +echo "==> Installing Python dependencies…" + +# experimental uv support makes installations significantly faster +rye config --set-bool behavior.use-uv=true + +rye sync --all-features diff --git a/scripts/format b/scripts/format new file mode 100755 index 00000000..667ec2d7 --- /dev/null +++ b/scripts/format @@ -0,0 +1,8 @@ +#!/usr/bin/env bash + +set -e + +cd "$(dirname "$0")/.." + +echo "==> Running formatters" +rye run format diff --git a/scripts/lint b/scripts/lint new file mode 100755 index 00000000..6c3a7b4e --- /dev/null +++ b/scripts/lint @@ -0,0 +1,11 @@ +#!/usr/bin/env bash + +set -e + +cd "$(dirname "$0")/.." + +echo "==> Running lints" +rye run lint + +echo "==> Making sure it imports" +rye run python -c 'import stagehand' diff --git a/scripts/mock b/scripts/mock new file mode 100755 index 00000000..0b28f6ea --- /dev/null +++ b/scripts/mock @@ -0,0 +1,41 @@ +#!/usr/bin/env bash + +set -e + +cd "$(dirname "$0")/.." + +if [[ -n "$1" && "$1" != '--'* ]]; then + URL="$1" + shift +else + URL="$(grep 'openapi_spec_url' .stats.yml | cut -d' ' -f2)" +fi + +# Check if the URL is empty +if [ -z "$URL" ]; then + echo "Error: No OpenAPI spec path/url provided or found in .stats.yml" + exit 1 +fi + +echo "==> Starting mock server with URL ${URL}" + +# Run prism mock on the given spec +if [ "$1" == "--daemon" ]; then + npm exec --package=@stainless-api/prism-cli@5.15.0 -- prism mock "$URL" &> .prism.log & + + # Wait for server to come online + echo -n "Waiting for server" + while ! grep -q "✖ fatal\|Prism is listening" ".prism.log" ; do + echo -n "." + sleep 0.1 + done + + if grep -q "✖ fatal" ".prism.log"; then + cat .prism.log + exit 1 + fi + + echo +else + npm exec --package=@stainless-api/prism-cli@5.15.0 -- prism mock "$URL" +fi diff --git a/scripts/test b/scripts/test new file mode 100755 index 00000000..dbeda2d2 --- /dev/null +++ b/scripts/test @@ -0,0 +1,61 @@ +#!/usr/bin/env bash + +set -e + +cd "$(dirname "$0")/.." + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[0;33m' +NC='\033[0m' # No Color + +function prism_is_running() { + curl --silent "http://localhost:4010" >/dev/null 2>&1 +} + +kill_server_on_port() { + pids=$(lsof -t -i tcp:"$1" || echo "") + if [ "$pids" != "" ]; then + kill "$pids" + echo "Stopped $pids." + fi +} + +function is_overriding_api_base_url() { + [ -n "$TEST_API_BASE_URL" ] +} + +if ! is_overriding_api_base_url && ! prism_is_running ; then + # When we exit this script, make sure to kill the background mock server process + trap 'kill_server_on_port 4010' EXIT + + # Start the dev server + ./scripts/mock --daemon +fi + +if is_overriding_api_base_url ; then + echo -e "${GREEN}✔ Running tests against ${TEST_API_BASE_URL}${NC}" + echo +elif ! prism_is_running ; then + echo -e "${RED}ERROR:${NC} The test suite will not run without a mock Prism server" + echo -e "running against your OpenAPI spec." + echo + echo -e "To run the server, pass in the path or url of your OpenAPI" + echo -e "spec to the prism command:" + echo + echo -e " \$ ${YELLOW}npm exec --package=@stainless-api/prism-cli@5.15.0 -- prism mock path/to/your.openapi.yml${NC}" + echo + + exit 1 +else + echo -e "${GREEN}✔ Mock prism server is running with your OpenAPI spec${NC}" + echo +fi + +export DEFER_PYDANTIC_BUILD=false + +echo "==> Running tests" +rye run pytest "$@" + +echo "==> Running Pydantic v1 tests" +rye run nox -s test-pydantic-v1 -- "$@" diff --git a/scripts/utils/ruffen-docs.py b/scripts/utils/ruffen-docs.py new file mode 100644 index 00000000..0cf2bd2f --- /dev/null +++ b/scripts/utils/ruffen-docs.py @@ -0,0 +1,167 @@ +# fork of https://github.com/asottile/blacken-docs adapted for ruff +from __future__ import annotations + +import re +import sys +import argparse +import textwrap +import contextlib +import subprocess +from typing import Match, Optional, Sequence, Generator, NamedTuple, cast + +MD_RE = re.compile( + r"(?P^(?P *)```\s*python\n)" r"(?P.*?)" r"(?P^(?P=indent)```\s*$)", + re.DOTALL | re.MULTILINE, +) +MD_PYCON_RE = re.compile( + r"(?P^(?P *)```\s*pycon\n)" r"(?P.*?)" r"(?P^(?P=indent)```.*$)", + re.DOTALL | re.MULTILINE, +) +PYCON_PREFIX = ">>> " +PYCON_CONTINUATION_PREFIX = "..." +PYCON_CONTINUATION_RE = re.compile( + rf"^{re.escape(PYCON_CONTINUATION_PREFIX)}( |$)", +) +DEFAULT_LINE_LENGTH = 100 + + +class CodeBlockError(NamedTuple): + offset: int + exc: Exception + + +def format_str( + src: str, +) -> tuple[str, Sequence[CodeBlockError]]: + errors: list[CodeBlockError] = [] + + @contextlib.contextmanager + def _collect_error(match: Match[str]) -> Generator[None, None, None]: + try: + yield + except Exception as e: + errors.append(CodeBlockError(match.start(), e)) + + def _md_match(match: Match[str]) -> str: + code = textwrap.dedent(match["code"]) + with _collect_error(match): + code = format_code_block(code) + code = textwrap.indent(code, match["indent"]) + return f"{match['before']}{code}{match['after']}" + + def _pycon_match(match: Match[str]) -> str: + code = "" + fragment = cast(Optional[str], None) + + def finish_fragment() -> None: + nonlocal code + nonlocal fragment + + if fragment is not None: + with _collect_error(match): + fragment = format_code_block(fragment) + fragment_lines = fragment.splitlines() + code += f"{PYCON_PREFIX}{fragment_lines[0]}\n" + for line in fragment_lines[1:]: + # Skip blank lines to handle Black adding a blank above + # functions within blocks. A blank line would end the REPL + # continuation prompt. + # + # >>> if True: + # ... def f(): + # ... pass + # ... + if line: + code += f"{PYCON_CONTINUATION_PREFIX} {line}\n" + if fragment_lines[-1].startswith(" "): + code += f"{PYCON_CONTINUATION_PREFIX}\n" + fragment = None + + indentation = None + for line in match["code"].splitlines(): + orig_line, line = line, line.lstrip() + if indentation is None and line: + indentation = len(orig_line) - len(line) + continuation_match = PYCON_CONTINUATION_RE.match(line) + if continuation_match and fragment is not None: + fragment += line[continuation_match.end() :] + "\n" + else: + finish_fragment() + if line.startswith(PYCON_PREFIX): + fragment = line[len(PYCON_PREFIX) :] + "\n" + else: + code += orig_line[indentation:] + "\n" + finish_fragment() + return code + + def _md_pycon_match(match: Match[str]) -> str: + code = _pycon_match(match) + code = textwrap.indent(code, match["indent"]) + return f"{match['before']}{code}{match['after']}" + + src = MD_RE.sub(_md_match, src) + src = MD_PYCON_RE.sub(_md_pycon_match, src) + return src, errors + + +def format_code_block(code: str) -> str: + return subprocess.check_output( + [ + sys.executable, + "-m", + "ruff", + "format", + "--stdin-filename=script.py", + f"--line-length={DEFAULT_LINE_LENGTH}", + ], + encoding="utf-8", + input=code, + ) + + +def format_file( + filename: str, + skip_errors: bool, +) -> int: + with open(filename, encoding="UTF-8") as f: + contents = f.read() + new_contents, errors = format_str(contents) + for error in errors: + lineno = contents[: error.offset].count("\n") + 1 + print(f"{filename}:{lineno}: code block parse error {error.exc}") + if errors and not skip_errors: + return 1 + if contents != new_contents: + print(f"{filename}: Rewriting...") + with open(filename, "w", encoding="UTF-8") as f: + f.write(new_contents) + return 0 + else: + return 0 + + +def main(argv: Sequence[str] | None = None) -> int: + parser = argparse.ArgumentParser() + parser.add_argument( + "-l", + "--line-length", + type=int, + default=DEFAULT_LINE_LENGTH, + ) + parser.add_argument( + "-S", + "--skip-string-normalization", + action="store_true", + ) + parser.add_argument("-E", "--skip-errors", action="store_true") + parser.add_argument("filenames", nargs="*") + args = parser.parse_args(argv) + + retv = 0 + for filename in args.filenames: + retv |= format_file(filename, skip_errors=args.skip_errors) + return retv + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/utils/upload-artifact.sh b/scripts/utils/upload-artifact.sh new file mode 100755 index 00000000..37e61d02 --- /dev/null +++ b/scripts/utils/upload-artifact.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash +set -exuo pipefail + +FILENAME=$(basename dist/*.whl) + +RESPONSE=$(curl -X POST "$URL?filename=$FILENAME" \ + -H "Authorization: Bearer $AUTH" \ + -H "Content-Type: application/json") + +SIGNED_URL=$(echo "$RESPONSE" | jq -r '.url') + +if [[ "$SIGNED_URL" == "null" ]]; then + echo -e "\033[31mFailed to get signed URL.\033[0m" + exit 1 +fi + +UPLOAD_RESPONSE=$(curl -v -X PUT \ + -H "Content-Type: binary/octet-stream" \ + --data-binary "@dist/$FILENAME" "$SIGNED_URL" 2>&1) + +if echo "$UPLOAD_RESPONSE" | grep -q "HTTP/[0-9.]* 200"; then + echo -e "\033[32mUploaded build to Stainless storage.\033[0m" + echo -e "\033[32mInstallation: pip install 'https://pkg.stainless.com/s/stagehand-python/$SHA/$FILENAME'\033[0m" +else + echo -e "\033[31mFailed to upload artifact.\033[0m" + exit 1 +fi diff --git a/src/stagehand/__init__.py b/src/stagehand/__init__.py new file mode 100644 index 00000000..44c6dfcc --- /dev/null +++ b/src/stagehand/__init__.py @@ -0,0 +1,104 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +import typing as _t + +from . import types +from ._types import NOT_GIVEN, Omit, NoneType, NotGiven, Transport, ProxiesTypes, omit, not_given +from ._utils import file_from_path +from ._client import ( + ENVIRONMENTS, + Client, + Stream, + Timeout, + Stagehand, + Transport, + AsyncClient, + AsyncStream, + AsyncStagehand, + RequestOptions, +) +from ._models import BaseModel +from ._version import __title__, __version__ +from ._response import APIResponse as APIResponse, AsyncAPIResponse as AsyncAPIResponse +from ._constants import DEFAULT_TIMEOUT, DEFAULT_MAX_RETRIES, DEFAULT_CONNECTION_LIMITS +from ._exceptions import ( + APIError, + ConflictError, + NotFoundError, + APIStatusError, + RateLimitError, + StagehandError, + APITimeoutError, + BadRequestError, + APIConnectionError, + AuthenticationError, + InternalServerError, + PermissionDeniedError, + UnprocessableEntityError, + APIResponseValidationError, +) +from ._base_client import DefaultHttpxClient, DefaultAioHttpClient, DefaultAsyncHttpxClient +from ._utils._logs import setup_logging as _setup_logging + +__all__ = [ + "types", + "__version__", + "__title__", + "NoneType", + "Transport", + "ProxiesTypes", + "NotGiven", + "NOT_GIVEN", + "not_given", + "Omit", + "omit", + "StagehandError", + "APIError", + "APIStatusError", + "APITimeoutError", + "APIConnectionError", + "APIResponseValidationError", + "BadRequestError", + "AuthenticationError", + "PermissionDeniedError", + "NotFoundError", + "ConflictError", + "UnprocessableEntityError", + "RateLimitError", + "InternalServerError", + "Timeout", + "RequestOptions", + "Client", + "AsyncClient", + "Stream", + "AsyncStream", + "Stagehand", + "AsyncStagehand", + "ENVIRONMENTS", + "file_from_path", + "BaseModel", + "DEFAULT_TIMEOUT", + "DEFAULT_MAX_RETRIES", + "DEFAULT_CONNECTION_LIMITS", + "DefaultHttpxClient", + "DefaultAsyncHttpxClient", + "DefaultAioHttpClient", +] + +if not _t.TYPE_CHECKING: + from ._utils._resources_proxy import resources as resources + +_setup_logging() + +# Update the __module__ attribute for exported symbols so that +# error messages point to this module instead of the module +# it was originally defined in, e.g. +# stagehand._exceptions.NotFoundError -> stagehand.NotFoundError +__locals = locals() +for __name in __all__: + if not __name.startswith("__"): + try: + __locals[__name].__module__ = "stagehand" + except (TypeError, AttributeError): + # Some of our exported symbols are builtins which we can't set attributes for. + pass diff --git a/src/stagehand/_base_client.py b/src/stagehand/_base_client.py new file mode 100644 index 00000000..e08b1de0 --- /dev/null +++ b/src/stagehand/_base_client.py @@ -0,0 +1,1995 @@ +from __future__ import annotations + +import sys +import json +import time +import uuid +import email +import asyncio +import inspect +import logging +import platform +import email.utils +from types import TracebackType +from random import random +from typing import ( + TYPE_CHECKING, + Any, + Dict, + Type, + Union, + Generic, + Mapping, + TypeVar, + Iterable, + Iterator, + Optional, + Generator, + AsyncIterator, + cast, + overload, +) +from typing_extensions import Literal, override, get_origin + +import anyio +import httpx +import distro +import pydantic +from httpx import URL +from pydantic import PrivateAttr + +from . import _exceptions +from ._qs import Querystring +from ._files import to_httpx_files, async_to_httpx_files +from ._types import ( + Body, + Omit, + Query, + Headers, + Timeout, + NotGiven, + ResponseT, + AnyMapping, + PostParser, + RequestFiles, + HttpxSendArgs, + RequestOptions, + HttpxRequestFiles, + ModelBuilderProtocol, + not_given, +) +from ._utils import is_dict, is_list, asyncify, is_given, lru_cache, is_mapping +from ._compat import PYDANTIC_V1, model_copy, model_dump +from ._models import GenericModel, FinalRequestOptions, validate_type, construct_type +from ._response import ( + APIResponse, + BaseAPIResponse, + AsyncAPIResponse, + extract_response_type, +) +from ._constants import ( + DEFAULT_TIMEOUT, + MAX_RETRY_DELAY, + DEFAULT_MAX_RETRIES, + INITIAL_RETRY_DELAY, + RAW_RESPONSE_HEADER, + OVERRIDE_CAST_TO_HEADER, + DEFAULT_CONNECTION_LIMITS, +) +from ._streaming import Stream, SSEDecoder, AsyncStream, SSEBytesDecoder +from ._exceptions import ( + APIStatusError, + APITimeoutError, + APIConnectionError, + APIResponseValidationError, +) + +log: logging.Logger = logging.getLogger(__name__) + +# TODO: make base page type vars covariant +SyncPageT = TypeVar("SyncPageT", bound="BaseSyncPage[Any]") +AsyncPageT = TypeVar("AsyncPageT", bound="BaseAsyncPage[Any]") + + +_T = TypeVar("_T") +_T_co = TypeVar("_T_co", covariant=True) + +_StreamT = TypeVar("_StreamT", bound=Stream[Any]) +_AsyncStreamT = TypeVar("_AsyncStreamT", bound=AsyncStream[Any]) + +if TYPE_CHECKING: + from httpx._config import ( + DEFAULT_TIMEOUT_CONFIG, # pyright: ignore[reportPrivateImportUsage] + ) + + HTTPX_DEFAULT_TIMEOUT = DEFAULT_TIMEOUT_CONFIG +else: + try: + from httpx._config import DEFAULT_TIMEOUT_CONFIG as HTTPX_DEFAULT_TIMEOUT + except ImportError: + # taken from https://github.com/encode/httpx/blob/3ba5fe0d7ac70222590e759c31442b1cab263791/httpx/_config.py#L366 + HTTPX_DEFAULT_TIMEOUT = Timeout(5.0) + + +class PageInfo: + """Stores the necessary information to build the request to retrieve the next page. + + Either `url` or `params` must be set. + """ + + url: URL | NotGiven + params: Query | NotGiven + json: Body | NotGiven + + @overload + def __init__( + self, + *, + url: URL, + ) -> None: ... + + @overload + def __init__( + self, + *, + params: Query, + ) -> None: ... + + @overload + def __init__( + self, + *, + json: Body, + ) -> None: ... + + def __init__( + self, + *, + url: URL | NotGiven = not_given, + json: Body | NotGiven = not_given, + params: Query | NotGiven = not_given, + ) -> None: + self.url = url + self.json = json + self.params = params + + @override + def __repr__(self) -> str: + if self.url: + return f"{self.__class__.__name__}(url={self.url})" + if self.json: + return f"{self.__class__.__name__}(json={self.json})" + return f"{self.__class__.__name__}(params={self.params})" + + +class BasePage(GenericModel, Generic[_T]): + """ + Defines the core interface for pagination. + + Type Args: + ModelT: The pydantic model that represents an item in the response. + + Methods: + has_next_page(): Check if there is another page available + next_page_info(): Get the necessary information to make a request for the next page + """ + + _options: FinalRequestOptions = PrivateAttr() + _model: Type[_T] = PrivateAttr() + + def has_next_page(self) -> bool: + items = self._get_page_items() + if not items: + return False + return self.next_page_info() is not None + + def next_page_info(self) -> Optional[PageInfo]: ... + + def _get_page_items(self) -> Iterable[_T]: # type: ignore[empty-body] + ... + + def _params_from_url(self, url: URL) -> httpx.QueryParams: + # TODO: do we have to preprocess params here? + return httpx.QueryParams(cast(Any, self._options.params)).merge(url.params) + + def _info_to_options(self, info: PageInfo) -> FinalRequestOptions: + options = model_copy(self._options) + options._strip_raw_response_header() + + if not isinstance(info.params, NotGiven): + options.params = {**options.params, **info.params} + return options + + if not isinstance(info.url, NotGiven): + params = self._params_from_url(info.url) + url = info.url.copy_with(params=params) + options.params = dict(url.params) + options.url = str(url) + return options + + if not isinstance(info.json, NotGiven): + if not is_mapping(info.json): + raise TypeError("Pagination is only supported with mappings") + + if not options.json_data: + options.json_data = {**info.json} + else: + if not is_mapping(options.json_data): + raise TypeError("Pagination is only supported with mappings") + + options.json_data = {**options.json_data, **info.json} + return options + + raise ValueError("Unexpected PageInfo state") + + +class BaseSyncPage(BasePage[_T], Generic[_T]): + _client: SyncAPIClient = pydantic.PrivateAttr() + + def _set_private_attributes( + self, + client: SyncAPIClient, + model: Type[_T], + options: FinalRequestOptions, + ) -> None: + if (not PYDANTIC_V1) and getattr(self, "__pydantic_private__", None) is None: + self.__pydantic_private__ = {} + + self._model = model + self._client = client + self._options = options + + # Pydantic uses a custom `__iter__` method to support casting BaseModels + # to dictionaries. e.g. dict(model). + # As we want to support `for item in page`, this is inherently incompatible + # with the default pydantic behaviour. It is not possible to support both + # use cases at once. Fortunately, this is not a big deal as all other pydantic + # methods should continue to work as expected as there is an alternative method + # to cast a model to a dictionary, model.dict(), which is used internally + # by pydantic. + def __iter__(self) -> Iterator[_T]: # type: ignore + for page in self.iter_pages(): + for item in page._get_page_items(): + yield item + + def iter_pages(self: SyncPageT) -> Iterator[SyncPageT]: + page = self + while True: + yield page + if page.has_next_page(): + page = page.get_next_page() + else: + return + + def get_next_page(self: SyncPageT) -> SyncPageT: + info = self.next_page_info() + if not info: + raise RuntimeError( + "No next page expected; please check `.has_next_page()` before calling `.get_next_page()`." + ) + + options = self._info_to_options(info) + return self._client._request_api_list(self._model, page=self.__class__, options=options) + + +class AsyncPaginator(Generic[_T, AsyncPageT]): + def __init__( + self, + client: AsyncAPIClient, + options: FinalRequestOptions, + page_cls: Type[AsyncPageT], + model: Type[_T], + ) -> None: + self._model = model + self._client = client + self._options = options + self._page_cls = page_cls + + def __await__(self) -> Generator[Any, None, AsyncPageT]: + return self._get_page().__await__() + + async def _get_page(self) -> AsyncPageT: + def _parser(resp: AsyncPageT) -> AsyncPageT: + resp._set_private_attributes( + model=self._model, + options=self._options, + client=self._client, + ) + return resp + + self._options.post_parser = _parser + + return await self._client.request(self._page_cls, self._options) + + async def __aiter__(self) -> AsyncIterator[_T]: + # https://github.com/microsoft/pyright/issues/3464 + page = cast( + AsyncPageT, + await self, # type: ignore + ) + async for item in page: + yield item + + +class BaseAsyncPage(BasePage[_T], Generic[_T]): + _client: AsyncAPIClient = pydantic.PrivateAttr() + + def _set_private_attributes( + self, + model: Type[_T], + client: AsyncAPIClient, + options: FinalRequestOptions, + ) -> None: + if (not PYDANTIC_V1) and getattr(self, "__pydantic_private__", None) is None: + self.__pydantic_private__ = {} + + self._model = model + self._client = client + self._options = options + + async def __aiter__(self) -> AsyncIterator[_T]: + async for page in self.iter_pages(): + for item in page._get_page_items(): + yield item + + async def iter_pages(self: AsyncPageT) -> AsyncIterator[AsyncPageT]: + page = self + while True: + yield page + if page.has_next_page(): + page = await page.get_next_page() + else: + return + + async def get_next_page(self: AsyncPageT) -> AsyncPageT: + info = self.next_page_info() + if not info: + raise RuntimeError( + "No next page expected; please check `.has_next_page()` before calling `.get_next_page()`." + ) + + options = self._info_to_options(info) + return await self._client._request_api_list(self._model, page=self.__class__, options=options) + + +_HttpxClientT = TypeVar("_HttpxClientT", bound=Union[httpx.Client, httpx.AsyncClient]) +_DefaultStreamT = TypeVar("_DefaultStreamT", bound=Union[Stream[Any], AsyncStream[Any]]) + + +class BaseClient(Generic[_HttpxClientT, _DefaultStreamT]): + _client: _HttpxClientT + _version: str + _base_url: URL + max_retries: int + timeout: Union[float, Timeout, None] + _strict_response_validation: bool + _idempotency_header: str | None + _default_stream_cls: type[_DefaultStreamT] | None = None + + def __init__( + self, + *, + version: str, + base_url: str | URL, + _strict_response_validation: bool, + max_retries: int = DEFAULT_MAX_RETRIES, + timeout: float | Timeout | None = DEFAULT_TIMEOUT, + custom_headers: Mapping[str, str] | None = None, + custom_query: Mapping[str, object] | None = None, + ) -> None: + self._version = version + self._base_url = self._enforce_trailing_slash(URL(base_url)) + self.max_retries = max_retries + self.timeout = timeout + self._custom_headers = custom_headers or {} + self._custom_query = custom_query or {} + self._strict_response_validation = _strict_response_validation + self._idempotency_header = None + self._platform: Platform | None = None + + if max_retries is None: # pyright: ignore[reportUnnecessaryComparison] + raise TypeError( + "max_retries cannot be None. If you want to disable retries, pass `0`; if you want unlimited retries, pass `math.inf` or a very high number; if you want the default behavior, pass `stagehand.DEFAULT_MAX_RETRIES`" + ) + + def _enforce_trailing_slash(self, url: URL) -> URL: + if url.raw_path.endswith(b"/"): + return url + return url.copy_with(raw_path=url.raw_path + b"/") + + def _make_status_error_from_response( + self, + response: httpx.Response, + ) -> APIStatusError: + if response.is_closed and not response.is_stream_consumed: + # We can't read the response body as it has been closed + # before it was read. This can happen if an event hook + # raises a status error. + body = None + err_msg = f"Error code: {response.status_code}" + else: + err_text = response.text.strip() + body = err_text + + try: + body = json.loads(err_text) + err_msg = f"Error code: {response.status_code} - {body}" + except Exception: + err_msg = err_text or f"Error code: {response.status_code}" + + return self._make_status_error(err_msg, body=body, response=response) + + def _make_status_error( + self, + err_msg: str, + *, + body: object, + response: httpx.Response, + ) -> _exceptions.APIStatusError: + raise NotImplementedError() + + def _build_headers(self, options: FinalRequestOptions, *, retries_taken: int = 0) -> httpx.Headers: + custom_headers = options.headers or {} + headers_dict = _merge_mappings(self.default_headers, custom_headers) + self._validate_headers(headers_dict, custom_headers) + + # headers are case-insensitive while dictionaries are not. + headers = httpx.Headers(headers_dict) + + idempotency_header = self._idempotency_header + if idempotency_header and options.idempotency_key and idempotency_header not in headers: + headers[idempotency_header] = options.idempotency_key + + # Don't set these headers if they were already set or removed by the caller. We check + # `custom_headers`, which can contain `Omit()`, instead of `headers` to account for the removal case. + lower_custom_headers = [header.lower() for header in custom_headers] + if "x-stainless-retry-count" not in lower_custom_headers: + headers["x-stainless-retry-count"] = str(retries_taken) + if "x-stainless-read-timeout" not in lower_custom_headers: + timeout = self.timeout if isinstance(options.timeout, NotGiven) else options.timeout + if isinstance(timeout, Timeout): + timeout = timeout.read + if timeout is not None: + headers["x-stainless-read-timeout"] = str(timeout) + + return headers + + def _prepare_url(self, url: str) -> URL: + """ + Merge a URL argument together with any 'base_url' on the client, + to create the URL used for the outgoing request. + """ + # Copied from httpx's `_merge_url` method. + merge_url = URL(url) + if merge_url.is_relative_url: + merge_raw_path = self.base_url.raw_path + merge_url.raw_path.lstrip(b"/") + return self.base_url.copy_with(raw_path=merge_raw_path) + + return merge_url + + def _make_sse_decoder(self) -> SSEDecoder | SSEBytesDecoder: + return SSEDecoder() + + def _build_request( + self, + options: FinalRequestOptions, + *, + retries_taken: int = 0, + ) -> httpx.Request: + if log.isEnabledFor(logging.DEBUG): + log.debug("Request options: %s", model_dump(options, exclude_unset=True)) + + kwargs: dict[str, Any] = {} + + json_data = options.json_data + if options.extra_json is not None: + if json_data is None: + json_data = cast(Body, options.extra_json) + elif is_mapping(json_data): + json_data = _merge_mappings(json_data, options.extra_json) + else: + raise RuntimeError(f"Unexpected JSON data type, {type(json_data)}, cannot merge with `extra_body`") + + headers = self._build_headers(options, retries_taken=retries_taken) + params = _merge_mappings(self.default_query, options.params) + content_type = headers.get("Content-Type") + files = options.files + + # If the given Content-Type header is multipart/form-data then it + # has to be removed so that httpx can generate the header with + # additional information for us as it has to be in this form + # for the server to be able to correctly parse the request: + # multipart/form-data; boundary=---abc-- + if content_type is not None and content_type.startswith("multipart/form-data"): + if "boundary" not in content_type: + # only remove the header if the boundary hasn't been explicitly set + # as the caller doesn't want httpx to come up with their own boundary + headers.pop("Content-Type") + + # As we are now sending multipart/form-data instead of application/json + # we need to tell httpx to use it, https://www.python-httpx.org/advanced/clients/#multipart-file-encoding + if json_data: + if not is_dict(json_data): + raise TypeError( + f"Expected query input to be a dictionary for multipart requests but got {type(json_data)} instead." + ) + kwargs["data"] = self._serialize_multipartform(json_data) + + # httpx determines whether or not to send a "multipart/form-data" + # request based on the truthiness of the "files" argument. + # This gets around that issue by generating a dict value that + # evaluates to true. + # + # https://github.com/encode/httpx/discussions/2399#discussioncomment-3814186 + if not files: + files = cast(HttpxRequestFiles, ForceMultipartDict()) + + prepared_url = self._prepare_url(options.url) + if "_" in prepared_url.host: + # work around https://github.com/encode/httpx/discussions/2880 + kwargs["extensions"] = {"sni_hostname": prepared_url.host.replace("_", "-")} + + is_body_allowed = options.method.lower() != "get" + + if is_body_allowed: + if isinstance(json_data, bytes): + kwargs["content"] = json_data + else: + kwargs["json"] = json_data if is_given(json_data) else None + kwargs["files"] = files + else: + headers.pop("Content-Type", None) + kwargs.pop("data", None) + + # TODO: report this error to httpx + return self._client.build_request( # pyright: ignore[reportUnknownMemberType] + headers=headers, + timeout=self.timeout if isinstance(options.timeout, NotGiven) else options.timeout, + method=options.method, + url=prepared_url, + # the `Query` type that we use is incompatible with qs' + # `Params` type as it needs to be typed as `Mapping[str, object]` + # so that passing a `TypedDict` doesn't cause an error. + # https://github.com/microsoft/pyright/issues/3526#event-6715453066 + params=self.qs.stringify(cast(Mapping[str, Any], params)) if params else None, + **kwargs, + ) + + def _serialize_multipartform(self, data: Mapping[object, object]) -> dict[str, object]: + items = self.qs.stringify_items( + # TODO: type ignore is required as stringify_items is well typed but we can't be + # well typed without heavy validation. + data, # type: ignore + array_format="brackets", + ) + serialized: dict[str, object] = {} + for key, value in items: + existing = serialized.get(key) + + if not existing: + serialized[key] = value + continue + + # If a value has already been set for this key then that + # means we're sending data like `array[]=[1, 2, 3]` and we + # need to tell httpx that we want to send multiple values with + # the same key which is done by using a list or a tuple. + # + # Note: 2d arrays should never result in the same key at both + # levels so it's safe to assume that if the value is a list, + # it was because we changed it to be a list. + if is_list(existing): + existing.append(value) + else: + serialized[key] = [existing, value] + + return serialized + + def _maybe_override_cast_to(self, cast_to: type[ResponseT], options: FinalRequestOptions) -> type[ResponseT]: + if not is_given(options.headers): + return cast_to + + # make a copy of the headers so we don't mutate user-input + headers = dict(options.headers) + + # we internally support defining a temporary header to override the + # default `cast_to` type for use with `.with_raw_response` and `.with_streaming_response` + # see _response.py for implementation details + override_cast_to = headers.pop(OVERRIDE_CAST_TO_HEADER, not_given) + if is_given(override_cast_to): + options.headers = headers + return cast(Type[ResponseT], override_cast_to) + + return cast_to + + def _should_stream_response_body(self, request: httpx.Request) -> bool: + return request.headers.get(RAW_RESPONSE_HEADER) == "stream" # type: ignore[no-any-return] + + def _process_response_data( + self, + *, + data: object, + cast_to: type[ResponseT], + response: httpx.Response, + ) -> ResponseT: + if data is None: + return cast(ResponseT, None) + + if cast_to is object: + return cast(ResponseT, data) + + try: + if inspect.isclass(cast_to) and issubclass(cast_to, ModelBuilderProtocol): + return cast(ResponseT, cast_to.build(response=response, data=data)) + + if self._strict_response_validation: + return cast(ResponseT, validate_type(type_=cast_to, value=data)) + + return cast(ResponseT, construct_type(type_=cast_to, value=data)) + except pydantic.ValidationError as err: + raise APIResponseValidationError(response=response, body=data) from err + + @property + def qs(self) -> Querystring: + return Querystring() + + @property + def custom_auth(self) -> httpx.Auth | None: + return None + + @property + def auth_headers(self) -> dict[str, str]: + return {} + + @property + def default_headers(self) -> dict[str, str | Omit]: + return { + "Accept": "application/json", + "Content-Type": "application/json", + "User-Agent": self.user_agent, + **self.platform_headers(), + **self.auth_headers, + **self._custom_headers, + } + + @property + def default_query(self) -> dict[str, object]: + return { + **self._custom_query, + } + + def _validate_headers( + self, + headers: Headers, # noqa: ARG002 + custom_headers: Headers, # noqa: ARG002 + ) -> None: + """Validate the given default headers and custom headers. + + Does nothing by default. + """ + return + + @property + def user_agent(self) -> str: + return f"{self.__class__.__name__}/Python {self._version}" + + @property + def base_url(self) -> URL: + return self._base_url + + @base_url.setter + def base_url(self, url: URL | str) -> None: + self._base_url = self._enforce_trailing_slash(url if isinstance(url, URL) else URL(url)) + + def platform_headers(self) -> Dict[str, str]: + # the actual implementation is in a separate `lru_cache` decorated + # function because adding `lru_cache` to methods will leak memory + # https://github.com/python/cpython/issues/88476 + return platform_headers(self._version, platform=self._platform) + + def _parse_retry_after_header(self, response_headers: Optional[httpx.Headers] = None) -> float | None: + """Returns a float of the number of seconds (not milliseconds) to wait after retrying, or None if unspecified. + + About the Retry-After header: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After + See also https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After#syntax + """ + if response_headers is None: + return None + + # First, try the non-standard `retry-after-ms` header for milliseconds, + # which is more precise than integer-seconds `retry-after` + try: + retry_ms_header = response_headers.get("retry-after-ms", None) + return float(retry_ms_header) / 1000 + except (TypeError, ValueError): + pass + + # Next, try parsing `retry-after` header as seconds (allowing nonstandard floats). + retry_header = response_headers.get("retry-after") + try: + # note: the spec indicates that this should only ever be an integer + # but if someone sends a float there's no reason for us to not respect it + return float(retry_header) + except (TypeError, ValueError): + pass + + # Last, try parsing `retry-after` as a date. + retry_date_tuple = email.utils.parsedate_tz(retry_header) + if retry_date_tuple is None: + return None + + retry_date = email.utils.mktime_tz(retry_date_tuple) + return float(retry_date - time.time()) + + def _calculate_retry_timeout( + self, + remaining_retries: int, + options: FinalRequestOptions, + response_headers: Optional[httpx.Headers] = None, + ) -> float: + max_retries = options.get_max_retries(self.max_retries) + + # If the API asks us to wait a certain amount of time (and it's a reasonable amount), just do what it says. + retry_after = self._parse_retry_after_header(response_headers) + if retry_after is not None and 0 < retry_after <= 60: + return retry_after + + # Also cap retry count to 1000 to avoid any potential overflows with `pow` + nb_retries = min(max_retries - remaining_retries, 1000) + + # Apply exponential backoff, but not more than the max. + sleep_seconds = min(INITIAL_RETRY_DELAY * pow(2.0, nb_retries), MAX_RETRY_DELAY) + + # Apply some jitter, plus-or-minus half a second. + jitter = 1 - 0.25 * random() + timeout = sleep_seconds * jitter + return timeout if timeout >= 0 else 0 + + def _should_retry(self, response: httpx.Response) -> bool: + # Note: this is not a standard header + should_retry_header = response.headers.get("x-should-retry") + + # If the server explicitly says whether or not to retry, obey. + if should_retry_header == "true": + log.debug("Retrying as header `x-should-retry` is set to `true`") + return True + if should_retry_header == "false": + log.debug("Not retrying as header `x-should-retry` is set to `false`") + return False + + # Retry on request timeouts. + if response.status_code == 408: + log.debug("Retrying due to status code %i", response.status_code) + return True + + # Retry on lock timeouts. + if response.status_code == 409: + log.debug("Retrying due to status code %i", response.status_code) + return True + + # Retry on rate limits. + if response.status_code == 429: + log.debug("Retrying due to status code %i", response.status_code) + return True + + # Retry internal errors. + if response.status_code >= 500: + log.debug("Retrying due to status code %i", response.status_code) + return True + + log.debug("Not retrying") + return False + + def _idempotency_key(self) -> str: + return f"stainless-python-retry-{uuid.uuid4()}" + + +class _DefaultHttpxClient(httpx.Client): + def __init__(self, **kwargs: Any) -> None: + kwargs.setdefault("timeout", DEFAULT_TIMEOUT) + kwargs.setdefault("limits", DEFAULT_CONNECTION_LIMITS) + kwargs.setdefault("follow_redirects", True) + super().__init__(**kwargs) + + +if TYPE_CHECKING: + DefaultHttpxClient = httpx.Client + """An alias to `httpx.Client` that provides the same defaults that this SDK + uses internally. + + This is useful because overriding the `http_client` with your own instance of + `httpx.Client` will result in httpx's defaults being used, not ours. + """ +else: + DefaultHttpxClient = _DefaultHttpxClient + + +class SyncHttpxClientWrapper(DefaultHttpxClient): + def __del__(self) -> None: + if self.is_closed: + return + + try: + self.close() + except Exception: + pass + + +class SyncAPIClient(BaseClient[httpx.Client, Stream[Any]]): + _client: httpx.Client + _default_stream_cls: type[Stream[Any]] | None = None + + def __init__( + self, + *, + version: str, + base_url: str | URL, + max_retries: int = DEFAULT_MAX_RETRIES, + timeout: float | Timeout | None | NotGiven = not_given, + http_client: httpx.Client | None = None, + custom_headers: Mapping[str, str] | None = None, + custom_query: Mapping[str, object] | None = None, + _strict_response_validation: bool, + ) -> None: + if not is_given(timeout): + # if the user passed in a custom http client with a non-default + # timeout set then we use that timeout. + # + # note: there is an edge case here where the user passes in a client + # where they've explicitly set the timeout to match the default timeout + # as this check is structural, meaning that we'll think they didn't + # pass in a timeout and will ignore it + if http_client and http_client.timeout != HTTPX_DEFAULT_TIMEOUT: + timeout = http_client.timeout + else: + timeout = DEFAULT_TIMEOUT + + if http_client is not None and not isinstance(http_client, httpx.Client): # pyright: ignore[reportUnnecessaryIsInstance] + raise TypeError( + f"Invalid `http_client` argument; Expected an instance of `httpx.Client` but got {type(http_client)}" + ) + + super().__init__( + version=version, + # cast to a valid type because mypy doesn't understand our type narrowing + timeout=cast(Timeout, timeout), + base_url=base_url, + max_retries=max_retries, + custom_query=custom_query, + custom_headers=custom_headers, + _strict_response_validation=_strict_response_validation, + ) + self._client = http_client or SyncHttpxClientWrapper( + base_url=base_url, + # cast to a valid type because mypy doesn't understand our type narrowing + timeout=cast(Timeout, timeout), + ) + + def is_closed(self) -> bool: + return self._client.is_closed + + def close(self) -> None: + """Close the underlying HTTPX client. + + The client will *not* be usable after this. + """ + # If an error is thrown while constructing a client, self._client + # may not be present + if hasattr(self, "_client"): + self._client.close() + + def __enter__(self: _T) -> _T: + return self + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc: BaseException | None, + exc_tb: TracebackType | None, + ) -> None: + self.close() + + def _prepare_options( + self, + options: FinalRequestOptions, # noqa: ARG002 + ) -> FinalRequestOptions: + """Hook for mutating the given options""" + return options + + def _prepare_request( + self, + request: httpx.Request, # noqa: ARG002 + ) -> None: + """This method is used as a callback for mutating the `Request` object + after it has been constructed. + This is useful for cases where you want to add certain headers based off of + the request properties, e.g. `url`, `method` etc. + """ + return None + + @overload + def request( + self, + cast_to: Type[ResponseT], + options: FinalRequestOptions, + *, + stream: Literal[True], + stream_cls: Type[_StreamT], + ) -> _StreamT: ... + + @overload + def request( + self, + cast_to: Type[ResponseT], + options: FinalRequestOptions, + *, + stream: Literal[False] = False, + ) -> ResponseT: ... + + @overload + def request( + self, + cast_to: Type[ResponseT], + options: FinalRequestOptions, + *, + stream: bool = False, + stream_cls: Type[_StreamT] | None = None, + ) -> ResponseT | _StreamT: ... + + def request( + self, + cast_to: Type[ResponseT], + options: FinalRequestOptions, + *, + stream: bool = False, + stream_cls: type[_StreamT] | None = None, + ) -> ResponseT | _StreamT: + cast_to = self._maybe_override_cast_to(cast_to, options) + + # create a copy of the options we were given so that if the + # options are mutated later & we then retry, the retries are + # given the original options + input_options = model_copy(options) + if input_options.idempotency_key is None and input_options.method.lower() != "get": + # ensure the idempotency key is reused between requests + input_options.idempotency_key = self._idempotency_key() + + response: httpx.Response | None = None + max_retries = input_options.get_max_retries(self.max_retries) + + retries_taken = 0 + for retries_taken in range(max_retries + 1): + options = model_copy(input_options) + options = self._prepare_options(options) + + remaining_retries = max_retries - retries_taken + request = self._build_request(options, retries_taken=retries_taken) + self._prepare_request(request) + + kwargs: HttpxSendArgs = {} + if self.custom_auth is not None: + kwargs["auth"] = self.custom_auth + + if options.follow_redirects is not None: + kwargs["follow_redirects"] = options.follow_redirects + + log.debug("Sending HTTP Request: %s %s", request.method, request.url) + + response = None + try: + response = self._client.send( + request, + stream=stream or self._should_stream_response_body(request=request), + **kwargs, + ) + except httpx.TimeoutException as err: + log.debug("Encountered httpx.TimeoutException", exc_info=True) + + if remaining_retries > 0: + self._sleep_for_retry( + retries_taken=retries_taken, + max_retries=max_retries, + options=input_options, + response=None, + ) + continue + + log.debug("Raising timeout error") + raise APITimeoutError(request=request) from err + except Exception as err: + log.debug("Encountered Exception", exc_info=True) + + if remaining_retries > 0: + self._sleep_for_retry( + retries_taken=retries_taken, + max_retries=max_retries, + options=input_options, + response=None, + ) + continue + + log.debug("Raising connection error") + raise APIConnectionError(request=request) from err + + log.debug( + 'HTTP Response: %s %s "%i %s" %s', + request.method, + request.url, + response.status_code, + response.reason_phrase, + response.headers, + ) + + try: + response.raise_for_status() + except httpx.HTTPStatusError as err: # thrown on 4xx and 5xx status code + log.debug("Encountered httpx.HTTPStatusError", exc_info=True) + + if remaining_retries > 0 and self._should_retry(err.response): + err.response.close() + self._sleep_for_retry( + retries_taken=retries_taken, + max_retries=max_retries, + options=input_options, + response=response, + ) + continue + + # If the response is streamed then we need to explicitly read the response + # to completion before attempting to access the response text. + if not err.response.is_closed: + err.response.read() + + log.debug("Re-raising status error") + raise self._make_status_error_from_response(err.response) from None + + break + + assert response is not None, "could not resolve response (should never happen)" + return self._process_response( + cast_to=cast_to, + options=options, + response=response, + stream=stream, + stream_cls=stream_cls, + retries_taken=retries_taken, + ) + + def _sleep_for_retry( + self, *, retries_taken: int, max_retries: int, options: FinalRequestOptions, response: httpx.Response | None + ) -> None: + remaining_retries = max_retries - retries_taken + if remaining_retries == 1: + log.debug("1 retry left") + else: + log.debug("%i retries left", remaining_retries) + + timeout = self._calculate_retry_timeout(remaining_retries, options, response.headers if response else None) + log.info("Retrying request to %s in %f seconds", options.url, timeout) + + time.sleep(timeout) + + def _process_response( + self, + *, + cast_to: Type[ResponseT], + options: FinalRequestOptions, + response: httpx.Response, + stream: bool, + stream_cls: type[Stream[Any]] | type[AsyncStream[Any]] | None, + retries_taken: int = 0, + ) -> ResponseT: + origin = get_origin(cast_to) or cast_to + + if ( + inspect.isclass(origin) + and issubclass(origin, BaseAPIResponse) + # we only want to actually return the custom BaseAPIResponse class if we're + # returning the raw response, or if we're not streaming SSE, as if we're streaming + # SSE then `cast_to` doesn't actively reflect the type we need to parse into + and (not stream or bool(response.request.headers.get(RAW_RESPONSE_HEADER))) + ): + if not issubclass(origin, APIResponse): + raise TypeError(f"API Response types must subclass {APIResponse}; Received {origin}") + + response_cls = cast("type[BaseAPIResponse[Any]]", cast_to) + return cast( + ResponseT, + response_cls( + raw=response, + client=self, + cast_to=extract_response_type(response_cls), + stream=stream, + stream_cls=stream_cls, + options=options, + retries_taken=retries_taken, + ), + ) + + if cast_to == httpx.Response: + return cast(ResponseT, response) + + api_response = APIResponse( + raw=response, + client=self, + cast_to=cast("type[ResponseT]", cast_to), # pyright: ignore[reportUnnecessaryCast] + stream=stream, + stream_cls=stream_cls, + options=options, + retries_taken=retries_taken, + ) + if bool(response.request.headers.get(RAW_RESPONSE_HEADER)): + return cast(ResponseT, api_response) + + return api_response.parse() + + def _request_api_list( + self, + model: Type[object], + page: Type[SyncPageT], + options: FinalRequestOptions, + ) -> SyncPageT: + def _parser(resp: SyncPageT) -> SyncPageT: + resp._set_private_attributes( + client=self, + model=model, + options=options, + ) + return resp + + options.post_parser = _parser + + return self.request(page, options, stream=False) + + @overload + def get( + self, + path: str, + *, + cast_to: Type[ResponseT], + options: RequestOptions = {}, + stream: Literal[False] = False, + ) -> ResponseT: ... + + @overload + def get( + self, + path: str, + *, + cast_to: Type[ResponseT], + options: RequestOptions = {}, + stream: Literal[True], + stream_cls: type[_StreamT], + ) -> _StreamT: ... + + @overload + def get( + self, + path: str, + *, + cast_to: Type[ResponseT], + options: RequestOptions = {}, + stream: bool, + stream_cls: type[_StreamT] | None = None, + ) -> ResponseT | _StreamT: ... + + def get( + self, + path: str, + *, + cast_to: Type[ResponseT], + options: RequestOptions = {}, + stream: bool = False, + stream_cls: type[_StreamT] | None = None, + ) -> ResponseT | _StreamT: + opts = FinalRequestOptions.construct(method="get", url=path, **options) + # cast is required because mypy complains about returning Any even though + # it understands the type variables + return cast(ResponseT, self.request(cast_to, opts, stream=stream, stream_cls=stream_cls)) + + @overload + def post( + self, + path: str, + *, + cast_to: Type[ResponseT], + body: Body | None = None, + options: RequestOptions = {}, + files: RequestFiles | None = None, + stream: Literal[False] = False, + ) -> ResponseT: ... + + @overload + def post( + self, + path: str, + *, + cast_to: Type[ResponseT], + body: Body | None = None, + options: RequestOptions = {}, + files: RequestFiles | None = None, + stream: Literal[True], + stream_cls: type[_StreamT], + ) -> _StreamT: ... + + @overload + def post( + self, + path: str, + *, + cast_to: Type[ResponseT], + body: Body | None = None, + options: RequestOptions = {}, + files: RequestFiles | None = None, + stream: bool, + stream_cls: type[_StreamT] | None = None, + ) -> ResponseT | _StreamT: ... + + def post( + self, + path: str, + *, + cast_to: Type[ResponseT], + body: Body | None = None, + options: RequestOptions = {}, + files: RequestFiles | None = None, + stream: bool = False, + stream_cls: type[_StreamT] | None = None, + ) -> ResponseT | _StreamT: + opts = FinalRequestOptions.construct( + method="post", url=path, json_data=body, files=to_httpx_files(files), **options + ) + return cast(ResponseT, self.request(cast_to, opts, stream=stream, stream_cls=stream_cls)) + + def patch( + self, + path: str, + *, + cast_to: Type[ResponseT], + body: Body | None = None, + options: RequestOptions = {}, + ) -> ResponseT: + opts = FinalRequestOptions.construct(method="patch", url=path, json_data=body, **options) + return self.request(cast_to, opts) + + def put( + self, + path: str, + *, + cast_to: Type[ResponseT], + body: Body | None = None, + files: RequestFiles | None = None, + options: RequestOptions = {}, + ) -> ResponseT: + opts = FinalRequestOptions.construct( + method="put", url=path, json_data=body, files=to_httpx_files(files), **options + ) + return self.request(cast_to, opts) + + def delete( + self, + path: str, + *, + cast_to: Type[ResponseT], + body: Body | None = None, + options: RequestOptions = {}, + ) -> ResponseT: + opts = FinalRequestOptions.construct(method="delete", url=path, json_data=body, **options) + return self.request(cast_to, opts) + + def get_api_list( + self, + path: str, + *, + model: Type[object], + page: Type[SyncPageT], + body: Body | None = None, + options: RequestOptions = {}, + method: str = "get", + ) -> SyncPageT: + opts = FinalRequestOptions.construct(method=method, url=path, json_data=body, **options) + return self._request_api_list(model, page, opts) + + +class _DefaultAsyncHttpxClient(httpx.AsyncClient): + def __init__(self, **kwargs: Any) -> None: + kwargs.setdefault("timeout", DEFAULT_TIMEOUT) + kwargs.setdefault("limits", DEFAULT_CONNECTION_LIMITS) + kwargs.setdefault("follow_redirects", True) + super().__init__(**kwargs) + + +try: + import httpx_aiohttp +except ImportError: + + class _DefaultAioHttpClient(httpx.AsyncClient): + def __init__(self, **_kwargs: Any) -> None: + raise RuntimeError("To use the aiohttp client you must have installed the package with the `aiohttp` extra") +else: + + class _DefaultAioHttpClient(httpx_aiohttp.HttpxAiohttpClient): # type: ignore + def __init__(self, **kwargs: Any) -> None: + kwargs.setdefault("timeout", DEFAULT_TIMEOUT) + kwargs.setdefault("limits", DEFAULT_CONNECTION_LIMITS) + kwargs.setdefault("follow_redirects", True) + + super().__init__(**kwargs) + + +if TYPE_CHECKING: + DefaultAsyncHttpxClient = httpx.AsyncClient + """An alias to `httpx.AsyncClient` that provides the same defaults that this SDK + uses internally. + + This is useful because overriding the `http_client` with your own instance of + `httpx.AsyncClient` will result in httpx's defaults being used, not ours. + """ + + DefaultAioHttpClient = httpx.AsyncClient + """An alias to `httpx.AsyncClient` that changes the default HTTP transport to `aiohttp`.""" +else: + DefaultAsyncHttpxClient = _DefaultAsyncHttpxClient + DefaultAioHttpClient = _DefaultAioHttpClient + + +class AsyncHttpxClientWrapper(DefaultAsyncHttpxClient): + def __del__(self) -> None: + if self.is_closed: + return + + try: + # TODO(someday): support non asyncio runtimes here + asyncio.get_running_loop().create_task(self.aclose()) + except Exception: + pass + + +class AsyncAPIClient(BaseClient[httpx.AsyncClient, AsyncStream[Any]]): + _client: httpx.AsyncClient + _default_stream_cls: type[AsyncStream[Any]] | None = None + + def __init__( + self, + *, + version: str, + base_url: str | URL, + _strict_response_validation: bool, + max_retries: int = DEFAULT_MAX_RETRIES, + timeout: float | Timeout | None | NotGiven = not_given, + http_client: httpx.AsyncClient | None = None, + custom_headers: Mapping[str, str] | None = None, + custom_query: Mapping[str, object] | None = None, + ) -> None: + if not is_given(timeout): + # if the user passed in a custom http client with a non-default + # timeout set then we use that timeout. + # + # note: there is an edge case here where the user passes in a client + # where they've explicitly set the timeout to match the default timeout + # as this check is structural, meaning that we'll think they didn't + # pass in a timeout and will ignore it + if http_client and http_client.timeout != HTTPX_DEFAULT_TIMEOUT: + timeout = http_client.timeout + else: + timeout = DEFAULT_TIMEOUT + + if http_client is not None and not isinstance(http_client, httpx.AsyncClient): # pyright: ignore[reportUnnecessaryIsInstance] + raise TypeError( + f"Invalid `http_client` argument; Expected an instance of `httpx.AsyncClient` but got {type(http_client)}" + ) + + super().__init__( + version=version, + base_url=base_url, + # cast to a valid type because mypy doesn't understand our type narrowing + timeout=cast(Timeout, timeout), + max_retries=max_retries, + custom_query=custom_query, + custom_headers=custom_headers, + _strict_response_validation=_strict_response_validation, + ) + self._client = http_client or AsyncHttpxClientWrapper( + base_url=base_url, + # cast to a valid type because mypy doesn't understand our type narrowing + timeout=cast(Timeout, timeout), + ) + + def is_closed(self) -> bool: + return self._client.is_closed + + async def close(self) -> None: + """Close the underlying HTTPX client. + + The client will *not* be usable after this. + """ + await self._client.aclose() + + async def __aenter__(self: _T) -> _T: + return self + + async def __aexit__( + self, + exc_type: type[BaseException] | None, + exc: BaseException | None, + exc_tb: TracebackType | None, + ) -> None: + await self.close() + + async def _prepare_options( + self, + options: FinalRequestOptions, # noqa: ARG002 + ) -> FinalRequestOptions: + """Hook for mutating the given options""" + return options + + async def _prepare_request( + self, + request: httpx.Request, # noqa: ARG002 + ) -> None: + """This method is used as a callback for mutating the `Request` object + after it has been constructed. + This is useful for cases where you want to add certain headers based off of + the request properties, e.g. `url`, `method` etc. + """ + return None + + @overload + async def request( + self, + cast_to: Type[ResponseT], + options: FinalRequestOptions, + *, + stream: Literal[False] = False, + ) -> ResponseT: ... + + @overload + async def request( + self, + cast_to: Type[ResponseT], + options: FinalRequestOptions, + *, + stream: Literal[True], + stream_cls: type[_AsyncStreamT], + ) -> _AsyncStreamT: ... + + @overload + async def request( + self, + cast_to: Type[ResponseT], + options: FinalRequestOptions, + *, + stream: bool, + stream_cls: type[_AsyncStreamT] | None = None, + ) -> ResponseT | _AsyncStreamT: ... + + async def request( + self, + cast_to: Type[ResponseT], + options: FinalRequestOptions, + *, + stream: bool = False, + stream_cls: type[_AsyncStreamT] | None = None, + ) -> ResponseT | _AsyncStreamT: + if self._platform is None: + # `get_platform` can make blocking IO calls so we + # execute it earlier while we are in an async context + self._platform = await asyncify(get_platform)() + + cast_to = self._maybe_override_cast_to(cast_to, options) + + # create a copy of the options we were given so that if the + # options are mutated later & we then retry, the retries are + # given the original options + input_options = model_copy(options) + if input_options.idempotency_key is None and input_options.method.lower() != "get": + # ensure the idempotency key is reused between requests + input_options.idempotency_key = self._idempotency_key() + + response: httpx.Response | None = None + max_retries = input_options.get_max_retries(self.max_retries) + + retries_taken = 0 + for retries_taken in range(max_retries + 1): + options = model_copy(input_options) + options = await self._prepare_options(options) + + remaining_retries = max_retries - retries_taken + request = self._build_request(options, retries_taken=retries_taken) + await self._prepare_request(request) + + kwargs: HttpxSendArgs = {} + if self.custom_auth is not None: + kwargs["auth"] = self.custom_auth + + if options.follow_redirects is not None: + kwargs["follow_redirects"] = options.follow_redirects + + log.debug("Sending HTTP Request: %s %s", request.method, request.url) + + response = None + try: + response = await self._client.send( + request, + stream=stream or self._should_stream_response_body(request=request), + **kwargs, + ) + except httpx.TimeoutException as err: + log.debug("Encountered httpx.TimeoutException", exc_info=True) + + if remaining_retries > 0: + await self._sleep_for_retry( + retries_taken=retries_taken, + max_retries=max_retries, + options=input_options, + response=None, + ) + continue + + log.debug("Raising timeout error") + raise APITimeoutError(request=request) from err + except Exception as err: + log.debug("Encountered Exception", exc_info=True) + + if remaining_retries > 0: + await self._sleep_for_retry( + retries_taken=retries_taken, + max_retries=max_retries, + options=input_options, + response=None, + ) + continue + + log.debug("Raising connection error") + raise APIConnectionError(request=request) from err + + log.debug( + 'HTTP Response: %s %s "%i %s" %s', + request.method, + request.url, + response.status_code, + response.reason_phrase, + response.headers, + ) + + try: + response.raise_for_status() + except httpx.HTTPStatusError as err: # thrown on 4xx and 5xx status code + log.debug("Encountered httpx.HTTPStatusError", exc_info=True) + + if remaining_retries > 0 and self._should_retry(err.response): + await err.response.aclose() + await self._sleep_for_retry( + retries_taken=retries_taken, + max_retries=max_retries, + options=input_options, + response=response, + ) + continue + + # If the response is streamed then we need to explicitly read the response + # to completion before attempting to access the response text. + if not err.response.is_closed: + await err.response.aread() + + log.debug("Re-raising status error") + raise self._make_status_error_from_response(err.response) from None + + break + + assert response is not None, "could not resolve response (should never happen)" + return await self._process_response( + cast_to=cast_to, + options=options, + response=response, + stream=stream, + stream_cls=stream_cls, + retries_taken=retries_taken, + ) + + async def _sleep_for_retry( + self, *, retries_taken: int, max_retries: int, options: FinalRequestOptions, response: httpx.Response | None + ) -> None: + remaining_retries = max_retries - retries_taken + if remaining_retries == 1: + log.debug("1 retry left") + else: + log.debug("%i retries left", remaining_retries) + + timeout = self._calculate_retry_timeout(remaining_retries, options, response.headers if response else None) + log.info("Retrying request to %s in %f seconds", options.url, timeout) + + await anyio.sleep(timeout) + + async def _process_response( + self, + *, + cast_to: Type[ResponseT], + options: FinalRequestOptions, + response: httpx.Response, + stream: bool, + stream_cls: type[Stream[Any]] | type[AsyncStream[Any]] | None, + retries_taken: int = 0, + ) -> ResponseT: + origin = get_origin(cast_to) or cast_to + + if ( + inspect.isclass(origin) + and issubclass(origin, BaseAPIResponse) + # we only want to actually return the custom BaseAPIResponse class if we're + # returning the raw response, or if we're not streaming SSE, as if we're streaming + # SSE then `cast_to` doesn't actively reflect the type we need to parse into + and (not stream or bool(response.request.headers.get(RAW_RESPONSE_HEADER))) + ): + if not issubclass(origin, AsyncAPIResponse): + raise TypeError(f"API Response types must subclass {AsyncAPIResponse}; Received {origin}") + + response_cls = cast("type[BaseAPIResponse[Any]]", cast_to) + return cast( + "ResponseT", + response_cls( + raw=response, + client=self, + cast_to=extract_response_type(response_cls), + stream=stream, + stream_cls=stream_cls, + options=options, + retries_taken=retries_taken, + ), + ) + + if cast_to == httpx.Response: + return cast(ResponseT, response) + + api_response = AsyncAPIResponse( + raw=response, + client=self, + cast_to=cast("type[ResponseT]", cast_to), # pyright: ignore[reportUnnecessaryCast] + stream=stream, + stream_cls=stream_cls, + options=options, + retries_taken=retries_taken, + ) + if bool(response.request.headers.get(RAW_RESPONSE_HEADER)): + return cast(ResponseT, api_response) + + return await api_response.parse() + + def _request_api_list( + self, + model: Type[_T], + page: Type[AsyncPageT], + options: FinalRequestOptions, + ) -> AsyncPaginator[_T, AsyncPageT]: + return AsyncPaginator(client=self, options=options, page_cls=page, model=model) + + @overload + async def get( + self, + path: str, + *, + cast_to: Type[ResponseT], + options: RequestOptions = {}, + stream: Literal[False] = False, + ) -> ResponseT: ... + + @overload + async def get( + self, + path: str, + *, + cast_to: Type[ResponseT], + options: RequestOptions = {}, + stream: Literal[True], + stream_cls: type[_AsyncStreamT], + ) -> _AsyncStreamT: ... + + @overload + async def get( + self, + path: str, + *, + cast_to: Type[ResponseT], + options: RequestOptions = {}, + stream: bool, + stream_cls: type[_AsyncStreamT] | None = None, + ) -> ResponseT | _AsyncStreamT: ... + + async def get( + self, + path: str, + *, + cast_to: Type[ResponseT], + options: RequestOptions = {}, + stream: bool = False, + stream_cls: type[_AsyncStreamT] | None = None, + ) -> ResponseT | _AsyncStreamT: + opts = FinalRequestOptions.construct(method="get", url=path, **options) + return await self.request(cast_to, opts, stream=stream, stream_cls=stream_cls) + + @overload + async def post( + self, + path: str, + *, + cast_to: Type[ResponseT], + body: Body | None = None, + files: RequestFiles | None = None, + options: RequestOptions = {}, + stream: Literal[False] = False, + ) -> ResponseT: ... + + @overload + async def post( + self, + path: str, + *, + cast_to: Type[ResponseT], + body: Body | None = None, + files: RequestFiles | None = None, + options: RequestOptions = {}, + stream: Literal[True], + stream_cls: type[_AsyncStreamT], + ) -> _AsyncStreamT: ... + + @overload + async def post( + self, + path: str, + *, + cast_to: Type[ResponseT], + body: Body | None = None, + files: RequestFiles | None = None, + options: RequestOptions = {}, + stream: bool, + stream_cls: type[_AsyncStreamT] | None = None, + ) -> ResponseT | _AsyncStreamT: ... + + async def post( + self, + path: str, + *, + cast_to: Type[ResponseT], + body: Body | None = None, + files: RequestFiles | None = None, + options: RequestOptions = {}, + stream: bool = False, + stream_cls: type[_AsyncStreamT] | None = None, + ) -> ResponseT | _AsyncStreamT: + opts = FinalRequestOptions.construct( + method="post", url=path, json_data=body, files=await async_to_httpx_files(files), **options + ) + return await self.request(cast_to, opts, stream=stream, stream_cls=stream_cls) + + async def patch( + self, + path: str, + *, + cast_to: Type[ResponseT], + body: Body | None = None, + options: RequestOptions = {}, + ) -> ResponseT: + opts = FinalRequestOptions.construct(method="patch", url=path, json_data=body, **options) + return await self.request(cast_to, opts) + + async def put( + self, + path: str, + *, + cast_to: Type[ResponseT], + body: Body | None = None, + files: RequestFiles | None = None, + options: RequestOptions = {}, + ) -> ResponseT: + opts = FinalRequestOptions.construct( + method="put", url=path, json_data=body, files=await async_to_httpx_files(files), **options + ) + return await self.request(cast_to, opts) + + async def delete( + self, + path: str, + *, + cast_to: Type[ResponseT], + body: Body | None = None, + options: RequestOptions = {}, + ) -> ResponseT: + opts = FinalRequestOptions.construct(method="delete", url=path, json_data=body, **options) + return await self.request(cast_to, opts) + + def get_api_list( + self, + path: str, + *, + model: Type[_T], + page: Type[AsyncPageT], + body: Body | None = None, + options: RequestOptions = {}, + method: str = "get", + ) -> AsyncPaginator[_T, AsyncPageT]: + opts = FinalRequestOptions.construct(method=method, url=path, json_data=body, **options) + return self._request_api_list(model, page, opts) + + +def make_request_options( + *, + query: Query | None = None, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + idempotency_key: str | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + post_parser: PostParser | NotGiven = not_given, +) -> RequestOptions: + """Create a dict of type RequestOptions without keys of NotGiven values.""" + options: RequestOptions = {} + if extra_headers is not None: + options["headers"] = extra_headers + + if extra_body is not None: + options["extra_json"] = cast(AnyMapping, extra_body) + + if query is not None: + options["params"] = query + + if extra_query is not None: + options["params"] = {**options.get("params", {}), **extra_query} + + if not isinstance(timeout, NotGiven): + options["timeout"] = timeout + + if idempotency_key is not None: + options["idempotency_key"] = idempotency_key + + if is_given(post_parser): + # internal + options["post_parser"] = post_parser # type: ignore + + return options + + +class ForceMultipartDict(Dict[str, None]): + def __bool__(self) -> bool: + return True + + +class OtherPlatform: + def __init__(self, name: str) -> None: + self.name = name + + @override + def __str__(self) -> str: + return f"Other:{self.name}" + + +Platform = Union[ + OtherPlatform, + Literal[ + "MacOS", + "Linux", + "Windows", + "FreeBSD", + "OpenBSD", + "iOS", + "Android", + "Unknown", + ], +] + + +def get_platform() -> Platform: + try: + system = platform.system().lower() + platform_name = platform.platform().lower() + except Exception: + return "Unknown" + + if "iphone" in platform_name or "ipad" in platform_name: + # Tested using Python3IDE on an iPhone 11 and Pythonista on an iPad 7 + # system is Darwin and platform_name is a string like: + # - Darwin-21.6.0-iPhone12,1-64bit + # - Darwin-21.6.0-iPad7,11-64bit + return "iOS" + + if system == "darwin": + return "MacOS" + + if system == "windows": + return "Windows" + + if "android" in platform_name: + # Tested using Pydroid 3 + # system is Linux and platform_name is a string like 'Linux-5.10.81-android12-9-00001-geba40aecb3b7-ab8534902-aarch64-with-libc' + return "Android" + + if system == "linux": + # https://distro.readthedocs.io/en/latest/#distro.id + distro_id = distro.id() + if distro_id == "freebsd": + return "FreeBSD" + + if distro_id == "openbsd": + return "OpenBSD" + + return "Linux" + + if platform_name: + return OtherPlatform(platform_name) + + return "Unknown" + + +@lru_cache(maxsize=None) +def platform_headers(version: str, *, platform: Platform | None) -> Dict[str, str]: + return { + "X-Stainless-Lang": "python", + "X-Stainless-Package-Version": version, + "X-Stainless-OS": str(platform or get_platform()), + "X-Stainless-Arch": str(get_architecture()), + "X-Stainless-Runtime": get_python_runtime(), + "X-Stainless-Runtime-Version": get_python_version(), + } + + +class OtherArch: + def __init__(self, name: str) -> None: + self.name = name + + @override + def __str__(self) -> str: + return f"other:{self.name}" + + +Arch = Union[OtherArch, Literal["x32", "x64", "arm", "arm64", "unknown"]] + + +def get_python_runtime() -> str: + try: + return platform.python_implementation() + except Exception: + return "unknown" + + +def get_python_version() -> str: + try: + return platform.python_version() + except Exception: + return "unknown" + + +def get_architecture() -> Arch: + try: + machine = platform.machine().lower() + except Exception: + return "unknown" + + if machine in ("arm64", "aarch64"): + return "arm64" + + # TODO: untested + if machine == "arm": + return "arm" + + if machine == "x86_64": + return "x64" + + # TODO: untested + if sys.maxsize <= 2**32: + return "x32" + + if machine: + return OtherArch(machine) + + return "unknown" + + +def _merge_mappings( + obj1: Mapping[_T_co, Union[_T, Omit]], + obj2: Mapping[_T_co, Union[_T, Omit]], +) -> Dict[_T_co, _T]: + """Merge two mappings of the same type, removing any values that are instances of `Omit`. + + In cases with duplicate keys the second mapping takes precedence. + """ + merged = {**obj1, **obj2} + return {key: value for key, value in merged.items() if not isinstance(value, Omit)} diff --git a/src/stagehand/_client.py b/src/stagehand/_client.py new file mode 100644 index 00000000..bf4d9773 --- /dev/null +++ b/src/stagehand/_client.py @@ -0,0 +1,480 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import os +from typing import Any, Dict, Mapping, cast +from typing_extensions import Self, Literal, override + +import httpx + +from . import _exceptions +from ._qs import Querystring +from ._types import ( + Omit, + Headers, + Timeout, + NotGiven, + Transport, + ProxiesTypes, + RequestOptions, + not_given, +) +from ._utils import is_given, get_async_library +from ._version import __version__ +from .resources import sessions +from ._streaming import Stream as Stream, AsyncStream as AsyncStream +from ._exceptions import APIStatusError +from ._base_client import ( + DEFAULT_MAX_RETRIES, + SyncAPIClient, + AsyncAPIClient, +) + +__all__ = [ + "ENVIRONMENTS", + "Timeout", + "Transport", + "ProxiesTypes", + "RequestOptions", + "Stagehand", + "AsyncStagehand", + "Client", + "AsyncClient", +] + +ENVIRONMENTS: Dict[str, str] = { + "production": "http://localhost:3000/v1", + "environment_1": "https://api.stagehand.browserbase.com/v1", +} + + +class Stagehand(SyncAPIClient): + sessions: sessions.SessionsResource + with_raw_response: StagehandWithRawResponse + with_streaming_response: StagehandWithStreamedResponse + + # client options + api_key: str | None + + _environment: Literal["production", "environment_1"] | NotGiven + + def __init__( + self, + *, + api_key: str | None = None, + environment: Literal["production", "environment_1"] | NotGiven = not_given, + base_url: str | httpx.URL | None | NotGiven = not_given, + timeout: float | Timeout | None | NotGiven = not_given, + max_retries: int = DEFAULT_MAX_RETRIES, + default_headers: Mapping[str, str] | None = None, + default_query: Mapping[str, object] | None = None, + # Configure a custom httpx client. + # We provide a `DefaultHttpxClient` class that you can pass to retain the default values we use for `limits`, `timeout` & `follow_redirects`. + # See the [httpx documentation](https://www.python-httpx.org/api/#client) for more details. + http_client: httpx.Client | None = None, + # Enable or disable schema validation for data returned by the API. + # When enabled an error APIResponseValidationError is raised + # if the API responds with invalid data for the expected schema. + # + # This parameter may be removed or changed in the future. + # If you rely on this feature, please open a GitHub issue + # outlining your use-case to help us decide if it should be + # part of our public interface in the future. + _strict_response_validation: bool = False, + ) -> None: + """Construct a new synchronous Stagehand client instance. + + This automatically infers the `api_key` argument from the `STAGEHAND_API_KEY` environment variable if it is not provided. + """ + if api_key is None: + api_key = os.environ.get("STAGEHAND_API_KEY") + self.api_key = api_key + + self._environment = environment + + base_url_env = os.environ.get("STAGEHAND_BASE_URL") + if is_given(base_url) and base_url is not None: + # cast required because mypy doesn't understand the type narrowing + base_url = cast("str | httpx.URL", base_url) # pyright: ignore[reportUnnecessaryCast] + elif is_given(environment): + if base_url_env and base_url is not None: + raise ValueError( + "Ambiguous URL; The `STAGEHAND_BASE_URL` env var and the `environment` argument are given. If you want to use the environment, you must pass base_url=None", + ) + + try: + base_url = ENVIRONMENTS[environment] + except KeyError as exc: + raise ValueError(f"Unknown environment: {environment}") from exc + elif base_url_env is not None: + base_url = base_url_env + else: + self._environment = environment = "production" + + try: + base_url = ENVIRONMENTS[environment] + except KeyError as exc: + raise ValueError(f"Unknown environment: {environment}") from exc + + super().__init__( + version=__version__, + base_url=base_url, + max_retries=max_retries, + timeout=timeout, + http_client=http_client, + custom_headers=default_headers, + custom_query=default_query, + _strict_response_validation=_strict_response_validation, + ) + + self.sessions = sessions.SessionsResource(self) + self.with_raw_response = StagehandWithRawResponse(self) + self.with_streaming_response = StagehandWithStreamedResponse(self) + + @property + @override + def qs(self) -> Querystring: + return Querystring(array_format="comma") + + @property + @override + def auth_headers(self) -> dict[str, str]: + api_key = self.api_key + if api_key is None: + return {} + return {"Authorization": f"Bearer {api_key}"} + + @property + @override + def default_headers(self) -> dict[str, str | Omit]: + return { + **super().default_headers, + "X-Stainless-Async": "false", + **self._custom_headers, + } + + @override + def _validate_headers(self, headers: Headers, custom_headers: Headers) -> None: + if self.api_key and headers.get("Authorization"): + return + if isinstance(custom_headers.get("Authorization"), Omit): + return + + raise TypeError( + '"Could not resolve authentication method. Expected the api_key to be set. Or for the `Authorization` headers to be explicitly omitted"' + ) + + def copy( + self, + *, + api_key: str | None = None, + environment: Literal["production", "environment_1"] | None = None, + base_url: str | httpx.URL | None = None, + timeout: float | Timeout | None | NotGiven = not_given, + http_client: httpx.Client | None = None, + max_retries: int | NotGiven = not_given, + default_headers: Mapping[str, str] | None = None, + set_default_headers: Mapping[str, str] | None = None, + default_query: Mapping[str, object] | None = None, + set_default_query: Mapping[str, object] | None = None, + _extra_kwargs: Mapping[str, Any] = {}, + ) -> Self: + """ + Create a new client instance re-using the same options given to the current client with optional overriding. + """ + if default_headers is not None and set_default_headers is not None: + raise ValueError("The `default_headers` and `set_default_headers` arguments are mutually exclusive") + + if default_query is not None and set_default_query is not None: + raise ValueError("The `default_query` and `set_default_query` arguments are mutually exclusive") + + headers = self._custom_headers + if default_headers is not None: + headers = {**headers, **default_headers} + elif set_default_headers is not None: + headers = set_default_headers + + params = self._custom_query + if default_query is not None: + params = {**params, **default_query} + elif set_default_query is not None: + params = set_default_query + + http_client = http_client or self._client + return self.__class__( + api_key=api_key or self.api_key, + base_url=base_url or self.base_url, + environment=environment or self._environment, + timeout=self.timeout if isinstance(timeout, NotGiven) else timeout, + http_client=http_client, + max_retries=max_retries if is_given(max_retries) else self.max_retries, + default_headers=headers, + default_query=params, + **_extra_kwargs, + ) + + # Alias for `copy` for nicer inline usage, e.g. + # client.with_options(timeout=10).foo.create(...) + with_options = copy + + @override + def _make_status_error( + self, + err_msg: str, + *, + body: object, + response: httpx.Response, + ) -> APIStatusError: + if response.status_code == 400: + return _exceptions.BadRequestError(err_msg, response=response, body=body) + + if response.status_code == 401: + return _exceptions.AuthenticationError(err_msg, response=response, body=body) + + if response.status_code == 403: + return _exceptions.PermissionDeniedError(err_msg, response=response, body=body) + + if response.status_code == 404: + return _exceptions.NotFoundError(err_msg, response=response, body=body) + + if response.status_code == 409: + return _exceptions.ConflictError(err_msg, response=response, body=body) + + if response.status_code == 422: + return _exceptions.UnprocessableEntityError(err_msg, response=response, body=body) + + if response.status_code == 429: + return _exceptions.RateLimitError(err_msg, response=response, body=body) + + if response.status_code >= 500: + return _exceptions.InternalServerError(err_msg, response=response, body=body) + return APIStatusError(err_msg, response=response, body=body) + + +class AsyncStagehand(AsyncAPIClient): + sessions: sessions.AsyncSessionsResource + with_raw_response: AsyncStagehandWithRawResponse + with_streaming_response: AsyncStagehandWithStreamedResponse + + # client options + api_key: str | None + + _environment: Literal["production", "environment_1"] | NotGiven + + def __init__( + self, + *, + api_key: str | None = None, + environment: Literal["production", "environment_1"] | NotGiven = not_given, + base_url: str | httpx.URL | None | NotGiven = not_given, + timeout: float | Timeout | None | NotGiven = not_given, + max_retries: int = DEFAULT_MAX_RETRIES, + default_headers: Mapping[str, str] | None = None, + default_query: Mapping[str, object] | None = None, + # Configure a custom httpx client. + # We provide a `DefaultAsyncHttpxClient` class that you can pass to retain the default values we use for `limits`, `timeout` & `follow_redirects`. + # See the [httpx documentation](https://www.python-httpx.org/api/#asyncclient) for more details. + http_client: httpx.AsyncClient | None = None, + # Enable or disable schema validation for data returned by the API. + # When enabled an error APIResponseValidationError is raised + # if the API responds with invalid data for the expected schema. + # + # This parameter may be removed or changed in the future. + # If you rely on this feature, please open a GitHub issue + # outlining your use-case to help us decide if it should be + # part of our public interface in the future. + _strict_response_validation: bool = False, + ) -> None: + """Construct a new async AsyncStagehand client instance. + + This automatically infers the `api_key` argument from the `STAGEHAND_API_KEY` environment variable if it is not provided. + """ + if api_key is None: + api_key = os.environ.get("STAGEHAND_API_KEY") + self.api_key = api_key + + self._environment = environment + + base_url_env = os.environ.get("STAGEHAND_BASE_URL") + if is_given(base_url) and base_url is not None: + # cast required because mypy doesn't understand the type narrowing + base_url = cast("str | httpx.URL", base_url) # pyright: ignore[reportUnnecessaryCast] + elif is_given(environment): + if base_url_env and base_url is not None: + raise ValueError( + "Ambiguous URL; The `STAGEHAND_BASE_URL` env var and the `environment` argument are given. If you want to use the environment, you must pass base_url=None", + ) + + try: + base_url = ENVIRONMENTS[environment] + except KeyError as exc: + raise ValueError(f"Unknown environment: {environment}") from exc + elif base_url_env is not None: + base_url = base_url_env + else: + self._environment = environment = "production" + + try: + base_url = ENVIRONMENTS[environment] + except KeyError as exc: + raise ValueError(f"Unknown environment: {environment}") from exc + + super().__init__( + version=__version__, + base_url=base_url, + max_retries=max_retries, + timeout=timeout, + http_client=http_client, + custom_headers=default_headers, + custom_query=default_query, + _strict_response_validation=_strict_response_validation, + ) + + self.sessions = sessions.AsyncSessionsResource(self) + self.with_raw_response = AsyncStagehandWithRawResponse(self) + self.with_streaming_response = AsyncStagehandWithStreamedResponse(self) + + @property + @override + def qs(self) -> Querystring: + return Querystring(array_format="comma") + + @property + @override + def auth_headers(self) -> dict[str, str]: + api_key = self.api_key + if api_key is None: + return {} + return {"Authorization": f"Bearer {api_key}"} + + @property + @override + def default_headers(self) -> dict[str, str | Omit]: + return { + **super().default_headers, + "X-Stainless-Async": f"async:{get_async_library()}", + **self._custom_headers, + } + + @override + def _validate_headers(self, headers: Headers, custom_headers: Headers) -> None: + if self.api_key and headers.get("Authorization"): + return + if isinstance(custom_headers.get("Authorization"), Omit): + return + + raise TypeError( + '"Could not resolve authentication method. Expected the api_key to be set. Or for the `Authorization` headers to be explicitly omitted"' + ) + + def copy( + self, + *, + api_key: str | None = None, + environment: Literal["production", "environment_1"] | None = None, + base_url: str | httpx.URL | None = None, + timeout: float | Timeout | None | NotGiven = not_given, + http_client: httpx.AsyncClient | None = None, + max_retries: int | NotGiven = not_given, + default_headers: Mapping[str, str] | None = None, + set_default_headers: Mapping[str, str] | None = None, + default_query: Mapping[str, object] | None = None, + set_default_query: Mapping[str, object] | None = None, + _extra_kwargs: Mapping[str, Any] = {}, + ) -> Self: + """ + Create a new client instance re-using the same options given to the current client with optional overriding. + """ + if default_headers is not None and set_default_headers is not None: + raise ValueError("The `default_headers` and `set_default_headers` arguments are mutually exclusive") + + if default_query is not None and set_default_query is not None: + raise ValueError("The `default_query` and `set_default_query` arguments are mutually exclusive") + + headers = self._custom_headers + if default_headers is not None: + headers = {**headers, **default_headers} + elif set_default_headers is not None: + headers = set_default_headers + + params = self._custom_query + if default_query is not None: + params = {**params, **default_query} + elif set_default_query is not None: + params = set_default_query + + http_client = http_client or self._client + return self.__class__( + api_key=api_key or self.api_key, + base_url=base_url or self.base_url, + environment=environment or self._environment, + timeout=self.timeout if isinstance(timeout, NotGiven) else timeout, + http_client=http_client, + max_retries=max_retries if is_given(max_retries) else self.max_retries, + default_headers=headers, + default_query=params, + **_extra_kwargs, + ) + + # Alias for `copy` for nicer inline usage, e.g. + # client.with_options(timeout=10).foo.create(...) + with_options = copy + + @override + def _make_status_error( + self, + err_msg: str, + *, + body: object, + response: httpx.Response, + ) -> APIStatusError: + if response.status_code == 400: + return _exceptions.BadRequestError(err_msg, response=response, body=body) + + if response.status_code == 401: + return _exceptions.AuthenticationError(err_msg, response=response, body=body) + + if response.status_code == 403: + return _exceptions.PermissionDeniedError(err_msg, response=response, body=body) + + if response.status_code == 404: + return _exceptions.NotFoundError(err_msg, response=response, body=body) + + if response.status_code == 409: + return _exceptions.ConflictError(err_msg, response=response, body=body) + + if response.status_code == 422: + return _exceptions.UnprocessableEntityError(err_msg, response=response, body=body) + + if response.status_code == 429: + return _exceptions.RateLimitError(err_msg, response=response, body=body) + + if response.status_code >= 500: + return _exceptions.InternalServerError(err_msg, response=response, body=body) + return APIStatusError(err_msg, response=response, body=body) + + +class StagehandWithRawResponse: + def __init__(self, client: Stagehand) -> None: + self.sessions = sessions.SessionsResourceWithRawResponse(client.sessions) + + +class AsyncStagehandWithRawResponse: + def __init__(self, client: AsyncStagehand) -> None: + self.sessions = sessions.AsyncSessionsResourceWithRawResponse(client.sessions) + + +class StagehandWithStreamedResponse: + def __init__(self, client: Stagehand) -> None: + self.sessions = sessions.SessionsResourceWithStreamingResponse(client.sessions) + + +class AsyncStagehandWithStreamedResponse: + def __init__(self, client: AsyncStagehand) -> None: + self.sessions = sessions.AsyncSessionsResourceWithStreamingResponse(client.sessions) + + +Client = Stagehand + +AsyncClient = AsyncStagehand diff --git a/src/stagehand/_compat.py b/src/stagehand/_compat.py new file mode 100644 index 00000000..bdef67f0 --- /dev/null +++ b/src/stagehand/_compat.py @@ -0,0 +1,219 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, Union, Generic, TypeVar, Callable, cast, overload +from datetime import date, datetime +from typing_extensions import Self, Literal + +import pydantic +from pydantic.fields import FieldInfo + +from ._types import IncEx, StrBytesIntFloat + +_T = TypeVar("_T") +_ModelT = TypeVar("_ModelT", bound=pydantic.BaseModel) + +# --------------- Pydantic v2, v3 compatibility --------------- + +# Pyright incorrectly reports some of our functions as overriding a method when they don't +# pyright: reportIncompatibleMethodOverride=false + +PYDANTIC_V1 = pydantic.VERSION.startswith("1.") + +if TYPE_CHECKING: + + def parse_date(value: date | StrBytesIntFloat) -> date: # noqa: ARG001 + ... + + def parse_datetime(value: Union[datetime, StrBytesIntFloat]) -> datetime: # noqa: ARG001 + ... + + def get_args(t: type[Any]) -> tuple[Any, ...]: # noqa: ARG001 + ... + + def is_union(tp: type[Any] | None) -> bool: # noqa: ARG001 + ... + + def get_origin(t: type[Any]) -> type[Any] | None: # noqa: ARG001 + ... + + def is_literal_type(type_: type[Any]) -> bool: # noqa: ARG001 + ... + + def is_typeddict(type_: type[Any]) -> bool: # noqa: ARG001 + ... + +else: + # v1 re-exports + if PYDANTIC_V1: + from pydantic.typing import ( + get_args as get_args, + is_union as is_union, + get_origin as get_origin, + is_typeddict as is_typeddict, + is_literal_type as is_literal_type, + ) + from pydantic.datetime_parse import parse_date as parse_date, parse_datetime as parse_datetime + else: + from ._utils import ( + get_args as get_args, + is_union as is_union, + get_origin as get_origin, + parse_date as parse_date, + is_typeddict as is_typeddict, + parse_datetime as parse_datetime, + is_literal_type as is_literal_type, + ) + + +# refactored config +if TYPE_CHECKING: + from pydantic import ConfigDict as ConfigDict +else: + if PYDANTIC_V1: + # TODO: provide an error message here? + ConfigDict = None + else: + from pydantic import ConfigDict as ConfigDict + + +# renamed methods / properties +def parse_obj(model: type[_ModelT], value: object) -> _ModelT: + if PYDANTIC_V1: + return cast(_ModelT, model.parse_obj(value)) # pyright: ignore[reportDeprecated, reportUnnecessaryCast] + else: + return model.model_validate(value) + + +def field_is_required(field: FieldInfo) -> bool: + if PYDANTIC_V1: + return field.required # type: ignore + return field.is_required() + + +def field_get_default(field: FieldInfo) -> Any: + value = field.get_default() + if PYDANTIC_V1: + return value + from pydantic_core import PydanticUndefined + + if value == PydanticUndefined: + return None + return value + + +def field_outer_type(field: FieldInfo) -> Any: + if PYDANTIC_V1: + return field.outer_type_ # type: ignore + return field.annotation + + +def get_model_config(model: type[pydantic.BaseModel]) -> Any: + if PYDANTIC_V1: + return model.__config__ # type: ignore + return model.model_config + + +def get_model_fields(model: type[pydantic.BaseModel]) -> dict[str, FieldInfo]: + if PYDANTIC_V1: + return model.__fields__ # type: ignore + return model.model_fields + + +def model_copy(model: _ModelT, *, deep: bool = False) -> _ModelT: + if PYDANTIC_V1: + return model.copy(deep=deep) # type: ignore + return model.model_copy(deep=deep) + + +def model_json(model: pydantic.BaseModel, *, indent: int | None = None) -> str: + if PYDANTIC_V1: + return model.json(indent=indent) # type: ignore + return model.model_dump_json(indent=indent) + + +def model_dump( + model: pydantic.BaseModel, + *, + exclude: IncEx | None = None, + exclude_unset: bool = False, + exclude_defaults: bool = False, + warnings: bool = True, + mode: Literal["json", "python"] = "python", +) -> dict[str, Any]: + if (not PYDANTIC_V1) or hasattr(model, "model_dump"): + return model.model_dump( + mode=mode, + exclude=exclude, + exclude_unset=exclude_unset, + exclude_defaults=exclude_defaults, + # warnings are not supported in Pydantic v1 + warnings=True if PYDANTIC_V1 else warnings, + ) + return cast( + "dict[str, Any]", + model.dict( # pyright: ignore[reportDeprecated, reportUnnecessaryCast] + exclude=exclude, + exclude_unset=exclude_unset, + exclude_defaults=exclude_defaults, + ), + ) + + +def model_parse(model: type[_ModelT], data: Any) -> _ModelT: + if PYDANTIC_V1: + return model.parse_obj(data) # pyright: ignore[reportDeprecated] + return model.model_validate(data) + + +# generic models +if TYPE_CHECKING: + + class GenericModel(pydantic.BaseModel): ... + +else: + if PYDANTIC_V1: + import pydantic.generics + + class GenericModel(pydantic.generics.GenericModel, pydantic.BaseModel): ... + else: + # there no longer needs to be a distinction in v2 but + # we still have to create our own subclass to avoid + # inconsistent MRO ordering errors + class GenericModel(pydantic.BaseModel): ... + + +# cached properties +if TYPE_CHECKING: + cached_property = property + + # we define a separate type (copied from typeshed) + # that represents that `cached_property` is `set`able + # at runtime, which differs from `@property`. + # + # this is a separate type as editors likely special case + # `@property` and we don't want to cause issues just to have + # more helpful internal types. + + class typed_cached_property(Generic[_T]): + func: Callable[[Any], _T] + attrname: str | None + + def __init__(self, func: Callable[[Any], _T]) -> None: ... + + @overload + def __get__(self, instance: None, owner: type[Any] | None = None) -> Self: ... + + @overload + def __get__(self, instance: object, owner: type[Any] | None = None) -> _T: ... + + def __get__(self, instance: object, owner: type[Any] | None = None) -> _T | Self: + raise NotImplementedError() + + def __set_name__(self, owner: type[Any], name: str) -> None: ... + + # __set__ is not defined at runtime, but @cached_property is designed to be settable + def __set__(self, instance: object, value: _T) -> None: ... +else: + from functools import cached_property as cached_property + + typed_cached_property = cached_property diff --git a/src/stagehand/_constants.py b/src/stagehand/_constants.py new file mode 100644 index 00000000..6ddf2c71 --- /dev/null +++ b/src/stagehand/_constants.py @@ -0,0 +1,14 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +import httpx + +RAW_RESPONSE_HEADER = "X-Stainless-Raw-Response" +OVERRIDE_CAST_TO_HEADER = "____stainless_override_cast_to" + +# default timeout is 1 minute +DEFAULT_TIMEOUT = httpx.Timeout(timeout=60, connect=5.0) +DEFAULT_MAX_RETRIES = 2 +DEFAULT_CONNECTION_LIMITS = httpx.Limits(max_connections=100, max_keepalive_connections=20) + +INITIAL_RETRY_DELAY = 0.5 +MAX_RETRY_DELAY = 8.0 diff --git a/src/stagehand/_exceptions.py b/src/stagehand/_exceptions.py new file mode 100644 index 00000000..52ee0590 --- /dev/null +++ b/src/stagehand/_exceptions.py @@ -0,0 +1,108 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Literal + +import httpx + +__all__ = [ + "BadRequestError", + "AuthenticationError", + "PermissionDeniedError", + "NotFoundError", + "ConflictError", + "UnprocessableEntityError", + "RateLimitError", + "InternalServerError", +] + + +class StagehandError(Exception): + pass + + +class APIError(StagehandError): + message: str + request: httpx.Request + + body: object | None + """The API response body. + + If the API responded with a valid JSON structure then this property will be the + decoded result. + + If it isn't a valid JSON structure then this will be the raw response. + + If there was no response associated with this error then it will be `None`. + """ + + def __init__(self, message: str, request: httpx.Request, *, body: object | None) -> None: # noqa: ARG002 + super().__init__(message) + self.request = request + self.message = message + self.body = body + + +class APIResponseValidationError(APIError): + response: httpx.Response + status_code: int + + def __init__(self, response: httpx.Response, body: object | None, *, message: str | None = None) -> None: + super().__init__(message or "Data returned by API invalid for expected schema.", response.request, body=body) + self.response = response + self.status_code = response.status_code + + +class APIStatusError(APIError): + """Raised when an API response has a status code of 4xx or 5xx.""" + + response: httpx.Response + status_code: int + + def __init__(self, message: str, *, response: httpx.Response, body: object | None) -> None: + super().__init__(message, response.request, body=body) + self.response = response + self.status_code = response.status_code + + +class APIConnectionError(APIError): + def __init__(self, *, message: str = "Connection error.", request: httpx.Request) -> None: + super().__init__(message, request, body=None) + + +class APITimeoutError(APIConnectionError): + def __init__(self, request: httpx.Request) -> None: + super().__init__(message="Request timed out.", request=request) + + +class BadRequestError(APIStatusError): + status_code: Literal[400] = 400 # pyright: ignore[reportIncompatibleVariableOverride] + + +class AuthenticationError(APIStatusError): + status_code: Literal[401] = 401 # pyright: ignore[reportIncompatibleVariableOverride] + + +class PermissionDeniedError(APIStatusError): + status_code: Literal[403] = 403 # pyright: ignore[reportIncompatibleVariableOverride] + + +class NotFoundError(APIStatusError): + status_code: Literal[404] = 404 # pyright: ignore[reportIncompatibleVariableOverride] + + +class ConflictError(APIStatusError): + status_code: Literal[409] = 409 # pyright: ignore[reportIncompatibleVariableOverride] + + +class UnprocessableEntityError(APIStatusError): + status_code: Literal[422] = 422 # pyright: ignore[reportIncompatibleVariableOverride] + + +class RateLimitError(APIStatusError): + status_code: Literal[429] = 429 # pyright: ignore[reportIncompatibleVariableOverride] + + +class InternalServerError(APIStatusError): + pass diff --git a/src/stagehand/_files.py b/src/stagehand/_files.py new file mode 100644 index 00000000..cc14c14f --- /dev/null +++ b/src/stagehand/_files.py @@ -0,0 +1,123 @@ +from __future__ import annotations + +import io +import os +import pathlib +from typing import overload +from typing_extensions import TypeGuard + +import anyio + +from ._types import ( + FileTypes, + FileContent, + RequestFiles, + HttpxFileTypes, + Base64FileInput, + HttpxFileContent, + HttpxRequestFiles, +) +from ._utils import is_tuple_t, is_mapping_t, is_sequence_t + + +def is_base64_file_input(obj: object) -> TypeGuard[Base64FileInput]: + return isinstance(obj, io.IOBase) or isinstance(obj, os.PathLike) + + +def is_file_content(obj: object) -> TypeGuard[FileContent]: + return ( + isinstance(obj, bytes) or isinstance(obj, tuple) or isinstance(obj, io.IOBase) or isinstance(obj, os.PathLike) + ) + + +def assert_is_file_content(obj: object, *, key: str | None = None) -> None: + if not is_file_content(obj): + prefix = f"Expected entry at `{key}`" if key is not None else f"Expected file input `{obj!r}`" + raise RuntimeError( + f"{prefix} to be bytes, an io.IOBase instance, PathLike or a tuple but received {type(obj)} instead." + ) from None + + +@overload +def to_httpx_files(files: None) -> None: ... + + +@overload +def to_httpx_files(files: RequestFiles) -> HttpxRequestFiles: ... + + +def to_httpx_files(files: RequestFiles | None) -> HttpxRequestFiles | None: + if files is None: + return None + + if is_mapping_t(files): + files = {key: _transform_file(file) for key, file in files.items()} + elif is_sequence_t(files): + files = [(key, _transform_file(file)) for key, file in files] + else: + raise TypeError(f"Unexpected file type input {type(files)}, expected mapping or sequence") + + return files + + +def _transform_file(file: FileTypes) -> HttpxFileTypes: + if is_file_content(file): + if isinstance(file, os.PathLike): + path = pathlib.Path(file) + return (path.name, path.read_bytes()) + + return file + + if is_tuple_t(file): + return (file[0], read_file_content(file[1]), *file[2:]) + + raise TypeError(f"Expected file types input to be a FileContent type or to be a tuple") + + +def read_file_content(file: FileContent) -> HttpxFileContent: + if isinstance(file, os.PathLike): + return pathlib.Path(file).read_bytes() + return file + + +@overload +async def async_to_httpx_files(files: None) -> None: ... + + +@overload +async def async_to_httpx_files(files: RequestFiles) -> HttpxRequestFiles: ... + + +async def async_to_httpx_files(files: RequestFiles | None) -> HttpxRequestFiles | None: + if files is None: + return None + + if is_mapping_t(files): + files = {key: await _async_transform_file(file) for key, file in files.items()} + elif is_sequence_t(files): + files = [(key, await _async_transform_file(file)) for key, file in files] + else: + raise TypeError("Unexpected file type input {type(files)}, expected mapping or sequence") + + return files + + +async def _async_transform_file(file: FileTypes) -> HttpxFileTypes: + if is_file_content(file): + if isinstance(file, os.PathLike): + path = anyio.Path(file) + return (path.name, await path.read_bytes()) + + return file + + if is_tuple_t(file): + return (file[0], await async_read_file_content(file[1]), *file[2:]) + + raise TypeError(f"Expected file types input to be a FileContent type or to be a tuple") + + +async def async_read_file_content(file: FileContent) -> HttpxFileContent: + if isinstance(file, os.PathLike): + return await anyio.Path(file).read_bytes() + + return file diff --git a/src/stagehand/_models.py b/src/stagehand/_models.py new file mode 100644 index 00000000..ca9500b2 --- /dev/null +++ b/src/stagehand/_models.py @@ -0,0 +1,857 @@ +from __future__ import annotations + +import os +import inspect +import weakref +from typing import TYPE_CHECKING, Any, Type, Union, Generic, TypeVar, Callable, Optional, cast +from datetime import date, datetime +from typing_extensions import ( + List, + Unpack, + Literal, + ClassVar, + Protocol, + Required, + ParamSpec, + TypedDict, + TypeGuard, + final, + override, + runtime_checkable, +) + +import pydantic +from pydantic.fields import FieldInfo + +from ._types import ( + Body, + IncEx, + Query, + ModelT, + Headers, + Timeout, + NotGiven, + AnyMapping, + HttpxRequestFiles, +) +from ._utils import ( + PropertyInfo, + is_list, + is_given, + json_safe, + lru_cache, + is_mapping, + parse_date, + coerce_boolean, + parse_datetime, + strip_not_given, + extract_type_arg, + is_annotated_type, + is_type_alias_type, + strip_annotated_type, +) +from ._compat import ( + PYDANTIC_V1, + ConfigDict, + GenericModel as BaseGenericModel, + get_args, + is_union, + parse_obj, + get_origin, + is_literal_type, + get_model_config, + get_model_fields, + field_get_default, +) +from ._constants import RAW_RESPONSE_HEADER + +if TYPE_CHECKING: + from pydantic_core.core_schema import ModelField, ModelSchema, LiteralSchema, ModelFieldsSchema + +__all__ = ["BaseModel", "GenericModel"] + +_T = TypeVar("_T") +_BaseModelT = TypeVar("_BaseModelT", bound="BaseModel") + +P = ParamSpec("P") + + +@runtime_checkable +class _ConfigProtocol(Protocol): + allow_population_by_field_name: bool + + +class BaseModel(pydantic.BaseModel): + if PYDANTIC_V1: + + @property + @override + def model_fields_set(self) -> set[str]: + # a forwards-compat shim for pydantic v2 + return self.__fields_set__ # type: ignore + + class Config(pydantic.BaseConfig): # pyright: ignore[reportDeprecated] + extra: Any = pydantic.Extra.allow # type: ignore + else: + model_config: ClassVar[ConfigDict] = ConfigDict( + extra="allow", defer_build=coerce_boolean(os.environ.get("DEFER_PYDANTIC_BUILD", "true")) + ) + + def to_dict( + self, + *, + mode: Literal["json", "python"] = "python", + use_api_names: bool = True, + exclude_unset: bool = True, + exclude_defaults: bool = False, + exclude_none: bool = False, + warnings: bool = True, + ) -> dict[str, object]: + """Recursively generate a dictionary representation of the model, optionally specifying which fields to include or exclude. + + By default, fields that were not set by the API will not be included, + and keys will match the API response, *not* the property names from the model. + + For example, if the API responds with `"fooBar": true` but we've defined a `foo_bar: bool` property, + the output will use the `"fooBar"` key (unless `use_api_names=False` is passed). + + Args: + mode: + If mode is 'json', the dictionary will only contain JSON serializable types. e.g. `datetime` will be turned into a string, `"2024-3-22T18:11:19.117000Z"`. + If mode is 'python', the dictionary may contain any Python objects. e.g. `datetime(2024, 3, 22)` + + use_api_names: Whether to use the key that the API responded with or the property name. Defaults to `True`. + exclude_unset: Whether to exclude fields that have not been explicitly set. + exclude_defaults: Whether to exclude fields that are set to their default value from the output. + exclude_none: Whether to exclude fields that have a value of `None` from the output. + warnings: Whether to log warnings when invalid fields are encountered. This is only supported in Pydantic v2. + """ + return self.model_dump( + mode=mode, + by_alias=use_api_names, + exclude_unset=exclude_unset, + exclude_defaults=exclude_defaults, + exclude_none=exclude_none, + warnings=warnings, + ) + + def to_json( + self, + *, + indent: int | None = 2, + use_api_names: bool = True, + exclude_unset: bool = True, + exclude_defaults: bool = False, + exclude_none: bool = False, + warnings: bool = True, + ) -> str: + """Generates a JSON string representing this model as it would be received from or sent to the API (but with indentation). + + By default, fields that were not set by the API will not be included, + and keys will match the API response, *not* the property names from the model. + + For example, if the API responds with `"fooBar": true` but we've defined a `foo_bar: bool` property, + the output will use the `"fooBar"` key (unless `use_api_names=False` is passed). + + Args: + indent: Indentation to use in the JSON output. If `None` is passed, the output will be compact. Defaults to `2` + use_api_names: Whether to use the key that the API responded with or the property name. Defaults to `True`. + exclude_unset: Whether to exclude fields that have not been explicitly set. + exclude_defaults: Whether to exclude fields that have the default value. + exclude_none: Whether to exclude fields that have a value of `None`. + warnings: Whether to show any warnings that occurred during serialization. This is only supported in Pydantic v2. + """ + return self.model_dump_json( + indent=indent, + by_alias=use_api_names, + exclude_unset=exclude_unset, + exclude_defaults=exclude_defaults, + exclude_none=exclude_none, + warnings=warnings, + ) + + @override + def __str__(self) -> str: + # mypy complains about an invalid self arg + return f"{self.__repr_name__()}({self.__repr_str__(', ')})" # type: ignore[misc] + + # Override the 'construct' method in a way that supports recursive parsing without validation. + # Based on https://github.com/samuelcolvin/pydantic/issues/1168#issuecomment-817742836. + @classmethod + @override + def construct( # pyright: ignore[reportIncompatibleMethodOverride] + __cls: Type[ModelT], + _fields_set: set[str] | None = None, + **values: object, + ) -> ModelT: + m = __cls.__new__(__cls) + fields_values: dict[str, object] = {} + + config = get_model_config(__cls) + populate_by_name = ( + config.allow_population_by_field_name + if isinstance(config, _ConfigProtocol) + else config.get("populate_by_name") + ) + + if _fields_set is None: + _fields_set = set() + + model_fields = get_model_fields(__cls) + for name, field in model_fields.items(): + key = field.alias + if key is None or (key not in values and populate_by_name): + key = name + + if key in values: + fields_values[name] = _construct_field(value=values[key], field=field, key=key) + _fields_set.add(name) + else: + fields_values[name] = field_get_default(field) + + extra_field_type = _get_extra_fields_type(__cls) + + _extra = {} + for key, value in values.items(): + if key not in model_fields: + parsed = construct_type(value=value, type_=extra_field_type) if extra_field_type is not None else value + + if PYDANTIC_V1: + _fields_set.add(key) + fields_values[key] = parsed + else: + _extra[key] = parsed + + object.__setattr__(m, "__dict__", fields_values) + + if PYDANTIC_V1: + # init_private_attributes() does not exist in v2 + m._init_private_attributes() # type: ignore + + # copied from Pydantic v1's `construct()` method + object.__setattr__(m, "__fields_set__", _fields_set) + else: + # these properties are copied from Pydantic's `model_construct()` method + object.__setattr__(m, "__pydantic_private__", None) + object.__setattr__(m, "__pydantic_extra__", _extra) + object.__setattr__(m, "__pydantic_fields_set__", _fields_set) + + return m + + if not TYPE_CHECKING: + # type checkers incorrectly complain about this assignment + # because the type signatures are technically different + # although not in practice + model_construct = construct + + if PYDANTIC_V1: + # we define aliases for some of the new pydantic v2 methods so + # that we can just document these methods without having to specify + # a specific pydantic version as some users may not know which + # pydantic version they are currently using + + @override + def model_dump( + self, + *, + mode: Literal["json", "python"] | str = "python", + include: IncEx | None = None, + exclude: IncEx | None = None, + context: Any | None = None, + by_alias: bool | None = None, + exclude_unset: bool = False, + exclude_defaults: bool = False, + exclude_none: bool = False, + exclude_computed_fields: bool = False, + round_trip: bool = False, + warnings: bool | Literal["none", "warn", "error"] = True, + fallback: Callable[[Any], Any] | None = None, + serialize_as_any: bool = False, + ) -> dict[str, Any]: + """Usage docs: https://docs.pydantic.dev/2.4/concepts/serialization/#modelmodel_dump + + Generate a dictionary representation of the model, optionally specifying which fields to include or exclude. + + Args: + mode: The mode in which `to_python` should run. + If mode is 'json', the output will only contain JSON serializable types. + If mode is 'python', the output may contain non-JSON-serializable Python objects. + include: A set of fields to include in the output. + exclude: A set of fields to exclude from the output. + context: Additional context to pass to the serializer. + by_alias: Whether to use the field's alias in the dictionary key if defined. + exclude_unset: Whether to exclude fields that have not been explicitly set. + exclude_defaults: Whether to exclude fields that are set to their default value. + exclude_none: Whether to exclude fields that have a value of `None`. + exclude_computed_fields: Whether to exclude computed fields. + While this can be useful for round-tripping, it is usually recommended to use the dedicated + `round_trip` parameter instead. + round_trip: If True, dumped values should be valid as input for non-idempotent types such as Json[T]. + warnings: How to handle serialization errors. False/"none" ignores them, True/"warn" logs errors, + "error" raises a [`PydanticSerializationError`][pydantic_core.PydanticSerializationError]. + fallback: A function to call when an unknown value is encountered. If not provided, + a [`PydanticSerializationError`][pydantic_core.PydanticSerializationError] error is raised. + serialize_as_any: Whether to serialize fields with duck-typing serialization behavior. + + Returns: + A dictionary representation of the model. + """ + if mode not in {"json", "python"}: + raise ValueError("mode must be either 'json' or 'python'") + if round_trip != False: + raise ValueError("round_trip is only supported in Pydantic v2") + if warnings != True: + raise ValueError("warnings is only supported in Pydantic v2") + if context is not None: + raise ValueError("context is only supported in Pydantic v2") + if serialize_as_any != False: + raise ValueError("serialize_as_any is only supported in Pydantic v2") + if fallback is not None: + raise ValueError("fallback is only supported in Pydantic v2") + if exclude_computed_fields != False: + raise ValueError("exclude_computed_fields is only supported in Pydantic v2") + dumped = super().dict( # pyright: ignore[reportDeprecated] + include=include, + exclude=exclude, + by_alias=by_alias if by_alias is not None else False, + exclude_unset=exclude_unset, + exclude_defaults=exclude_defaults, + exclude_none=exclude_none, + ) + + return cast("dict[str, Any]", json_safe(dumped)) if mode == "json" else dumped + + @override + def model_dump_json( + self, + *, + indent: int | None = None, + ensure_ascii: bool = False, + include: IncEx | None = None, + exclude: IncEx | None = None, + context: Any | None = None, + by_alias: bool | None = None, + exclude_unset: bool = False, + exclude_defaults: bool = False, + exclude_none: bool = False, + exclude_computed_fields: bool = False, + round_trip: bool = False, + warnings: bool | Literal["none", "warn", "error"] = True, + fallback: Callable[[Any], Any] | None = None, + serialize_as_any: bool = False, + ) -> str: + """Usage docs: https://docs.pydantic.dev/2.4/concepts/serialization/#modelmodel_dump_json + + Generates a JSON representation of the model using Pydantic's `to_json` method. + + Args: + indent: Indentation to use in the JSON output. If None is passed, the output will be compact. + include: Field(s) to include in the JSON output. Can take either a string or set of strings. + exclude: Field(s) to exclude from the JSON output. Can take either a string or set of strings. + by_alias: Whether to serialize using field aliases. + exclude_unset: Whether to exclude fields that have not been explicitly set. + exclude_defaults: Whether to exclude fields that have the default value. + exclude_none: Whether to exclude fields that have a value of `None`. + round_trip: Whether to use serialization/deserialization between JSON and class instance. + warnings: Whether to show any warnings that occurred during serialization. + + Returns: + A JSON string representation of the model. + """ + if round_trip != False: + raise ValueError("round_trip is only supported in Pydantic v2") + if warnings != True: + raise ValueError("warnings is only supported in Pydantic v2") + if context is not None: + raise ValueError("context is only supported in Pydantic v2") + if serialize_as_any != False: + raise ValueError("serialize_as_any is only supported in Pydantic v2") + if fallback is not None: + raise ValueError("fallback is only supported in Pydantic v2") + if ensure_ascii != False: + raise ValueError("ensure_ascii is only supported in Pydantic v2") + if exclude_computed_fields != False: + raise ValueError("exclude_computed_fields is only supported in Pydantic v2") + return super().json( # type: ignore[reportDeprecated] + indent=indent, + include=include, + exclude=exclude, + by_alias=by_alias if by_alias is not None else False, + exclude_unset=exclude_unset, + exclude_defaults=exclude_defaults, + exclude_none=exclude_none, + ) + + +def _construct_field(value: object, field: FieldInfo, key: str) -> object: + if value is None: + return field_get_default(field) + + if PYDANTIC_V1: + type_ = cast(type, field.outer_type_) # type: ignore + else: + type_ = field.annotation # type: ignore + + if type_ is None: + raise RuntimeError(f"Unexpected field type is None for {key}") + + return construct_type(value=value, type_=type_, metadata=getattr(field, "metadata", None)) + + +def _get_extra_fields_type(cls: type[pydantic.BaseModel]) -> type | None: + if PYDANTIC_V1: + # TODO + return None + + schema = cls.__pydantic_core_schema__ + if schema["type"] == "model": + fields = schema["schema"] + if fields["type"] == "model-fields": + extras = fields.get("extras_schema") + if extras and "cls" in extras: + # mypy can't narrow the type + return extras["cls"] # type: ignore[no-any-return] + + return None + + +def is_basemodel(type_: type) -> bool: + """Returns whether or not the given type is either a `BaseModel` or a union of `BaseModel`""" + if is_union(type_): + for variant in get_args(type_): + if is_basemodel(variant): + return True + + return False + + return is_basemodel_type(type_) + + +def is_basemodel_type(type_: type) -> TypeGuard[type[BaseModel] | type[GenericModel]]: + origin = get_origin(type_) or type_ + if not inspect.isclass(origin): + return False + return issubclass(origin, BaseModel) or issubclass(origin, GenericModel) + + +def build( + base_model_cls: Callable[P, _BaseModelT], + *args: P.args, + **kwargs: P.kwargs, +) -> _BaseModelT: + """Construct a BaseModel class without validation. + + This is useful for cases where you need to instantiate a `BaseModel` + from an API response as this provides type-safe params which isn't supported + by helpers like `construct_type()`. + + ```py + build(MyModel, my_field_a="foo", my_field_b=123) + ``` + """ + if args: + raise TypeError( + "Received positional arguments which are not supported; Keyword arguments must be used instead", + ) + + return cast(_BaseModelT, construct_type(type_=base_model_cls, value=kwargs)) + + +def construct_type_unchecked(*, value: object, type_: type[_T]) -> _T: + """Loose coercion to the expected type with construction of nested values. + + Note: the returned value from this function is not guaranteed to match the + given type. + """ + return cast(_T, construct_type(value=value, type_=type_)) + + +def construct_type(*, value: object, type_: object, metadata: Optional[List[Any]] = None) -> object: + """Loose coercion to the expected type with construction of nested values. + + If the given value does not match the expected type then it is returned as-is. + """ + + # store a reference to the original type we were given before we extract any inner + # types so that we can properly resolve forward references in `TypeAliasType` annotations + original_type = None + + # we allow `object` as the input type because otherwise, passing things like + # `Literal['value']` will be reported as a type error by type checkers + type_ = cast("type[object]", type_) + if is_type_alias_type(type_): + original_type = type_ # type: ignore[unreachable] + type_ = type_.__value__ # type: ignore[unreachable] + + # unwrap `Annotated[T, ...]` -> `T` + if metadata is not None and len(metadata) > 0: + meta: tuple[Any, ...] = tuple(metadata) + elif is_annotated_type(type_): + meta = get_args(type_)[1:] + type_ = extract_type_arg(type_, 0) + else: + meta = tuple() + + # we need to use the origin class for any types that are subscripted generics + # e.g. Dict[str, object] + origin = get_origin(type_) or type_ + args = get_args(type_) + + if is_union(origin): + try: + return validate_type(type_=cast("type[object]", original_type or type_), value=value) + except Exception: + pass + + # if the type is a discriminated union then we want to construct the right variant + # in the union, even if the data doesn't match exactly, otherwise we'd break code + # that relies on the constructed class types, e.g. + # + # class FooType: + # kind: Literal['foo'] + # value: str + # + # class BarType: + # kind: Literal['bar'] + # value: int + # + # without this block, if the data we get is something like `{'kind': 'bar', 'value': 'foo'}` then + # we'd end up constructing `FooType` when it should be `BarType`. + discriminator = _build_discriminated_union_meta(union=type_, meta_annotations=meta) + if discriminator and is_mapping(value): + variant_value = value.get(discriminator.field_alias_from or discriminator.field_name) + if variant_value and isinstance(variant_value, str): + variant_type = discriminator.mapping.get(variant_value) + if variant_type: + return construct_type(type_=variant_type, value=value) + + # if the data is not valid, use the first variant that doesn't fail while deserializing + for variant in args: + try: + return construct_type(value=value, type_=variant) + except Exception: + continue + + raise RuntimeError(f"Could not convert data into a valid instance of {type_}") + + if origin == dict: + if not is_mapping(value): + return value + + _, items_type = get_args(type_) # Dict[_, items_type] + return {key: construct_type(value=item, type_=items_type) for key, item in value.items()} + + if ( + not is_literal_type(type_) + and inspect.isclass(origin) + and (issubclass(origin, BaseModel) or issubclass(origin, GenericModel)) + ): + if is_list(value): + return [cast(Any, type_).construct(**entry) if is_mapping(entry) else entry for entry in value] + + if is_mapping(value): + if issubclass(type_, BaseModel): + return type_.construct(**value) # type: ignore[arg-type] + + return cast(Any, type_).construct(**value) + + if origin == list: + if not is_list(value): + return value + + inner_type = args[0] # List[inner_type] + return [construct_type(value=entry, type_=inner_type) for entry in value] + + if origin == float: + if isinstance(value, int): + coerced = float(value) + if coerced != value: + return value + return coerced + + return value + + if type_ == datetime: + try: + return parse_datetime(value) # type: ignore + except Exception: + return value + + if type_ == date: + try: + return parse_date(value) # type: ignore + except Exception: + return value + + return value + + +@runtime_checkable +class CachedDiscriminatorType(Protocol): + __discriminator__: DiscriminatorDetails + + +DISCRIMINATOR_CACHE: weakref.WeakKeyDictionary[type, DiscriminatorDetails] = weakref.WeakKeyDictionary() + + +class DiscriminatorDetails: + field_name: str + """The name of the discriminator field in the variant class, e.g. + + ```py + class Foo(BaseModel): + type: Literal['foo'] + ``` + + Will result in field_name='type' + """ + + field_alias_from: str | None + """The name of the discriminator field in the API response, e.g. + + ```py + class Foo(BaseModel): + type: Literal['foo'] = Field(alias='type_from_api') + ``` + + Will result in field_alias_from='type_from_api' + """ + + mapping: dict[str, type] + """Mapping of discriminator value to variant type, e.g. + + {'foo': FooVariant, 'bar': BarVariant} + """ + + def __init__( + self, + *, + mapping: dict[str, type], + discriminator_field: str, + discriminator_alias: str | None, + ) -> None: + self.mapping = mapping + self.field_name = discriminator_field + self.field_alias_from = discriminator_alias + + +def _build_discriminated_union_meta(*, union: type, meta_annotations: tuple[Any, ...]) -> DiscriminatorDetails | None: + cached = DISCRIMINATOR_CACHE.get(union) + if cached is not None: + return cached + + discriminator_field_name: str | None = None + + for annotation in meta_annotations: + if isinstance(annotation, PropertyInfo) and annotation.discriminator is not None: + discriminator_field_name = annotation.discriminator + break + + if not discriminator_field_name: + return None + + mapping: dict[str, type] = {} + discriminator_alias: str | None = None + + for variant in get_args(union): + variant = strip_annotated_type(variant) + if is_basemodel_type(variant): + if PYDANTIC_V1: + field_info = cast("dict[str, FieldInfo]", variant.__fields__).get(discriminator_field_name) # pyright: ignore[reportDeprecated, reportUnnecessaryCast] + if not field_info: + continue + + # Note: if one variant defines an alias then they all should + discriminator_alias = field_info.alias + + if (annotation := getattr(field_info, "annotation", None)) and is_literal_type(annotation): + for entry in get_args(annotation): + if isinstance(entry, str): + mapping[entry] = variant + else: + field = _extract_field_schema_pv2(variant, discriminator_field_name) + if not field: + continue + + # Note: if one variant defines an alias then they all should + discriminator_alias = field.get("serialization_alias") + + field_schema = field["schema"] + + if field_schema["type"] == "literal": + for entry in cast("LiteralSchema", field_schema)["expected"]: + if isinstance(entry, str): + mapping[entry] = variant + + if not mapping: + return None + + details = DiscriminatorDetails( + mapping=mapping, + discriminator_field=discriminator_field_name, + discriminator_alias=discriminator_alias, + ) + DISCRIMINATOR_CACHE.setdefault(union, details) + return details + + +def _extract_field_schema_pv2(model: type[BaseModel], field_name: str) -> ModelField | None: + schema = model.__pydantic_core_schema__ + if schema["type"] == "definitions": + schema = schema["schema"] + + if schema["type"] != "model": + return None + + schema = cast("ModelSchema", schema) + fields_schema = schema["schema"] + if fields_schema["type"] != "model-fields": + return None + + fields_schema = cast("ModelFieldsSchema", fields_schema) + field = fields_schema["fields"].get(field_name) + if not field: + return None + + return cast("ModelField", field) # pyright: ignore[reportUnnecessaryCast] + + +def validate_type(*, type_: type[_T], value: object) -> _T: + """Strict validation that the given value matches the expected type""" + if inspect.isclass(type_) and issubclass(type_, pydantic.BaseModel): + return cast(_T, parse_obj(type_, value)) + + return cast(_T, _validate_non_model_type(type_=type_, value=value)) + + +def set_pydantic_config(typ: Any, config: pydantic.ConfigDict) -> None: + """Add a pydantic config for the given type. + + Note: this is a no-op on Pydantic v1. + """ + setattr(typ, "__pydantic_config__", config) # noqa: B010 + + +# our use of subclassing here causes weirdness for type checkers, +# so we just pretend that we don't subclass +if TYPE_CHECKING: + GenericModel = BaseModel +else: + + class GenericModel(BaseGenericModel, BaseModel): + pass + + +if not PYDANTIC_V1: + from pydantic import TypeAdapter as _TypeAdapter + + _CachedTypeAdapter = cast("TypeAdapter[object]", lru_cache(maxsize=None)(_TypeAdapter)) + + if TYPE_CHECKING: + from pydantic import TypeAdapter + else: + TypeAdapter = _CachedTypeAdapter + + def _validate_non_model_type(*, type_: type[_T], value: object) -> _T: + return TypeAdapter(type_).validate_python(value) + +elif not TYPE_CHECKING: # TODO: condition is weird + + class RootModel(GenericModel, Generic[_T]): + """Used as a placeholder to easily convert runtime types to a Pydantic format + to provide validation. + + For example: + ```py + validated = RootModel[int](__root__="5").__root__ + # validated: 5 + ``` + """ + + __root__: _T + + def _validate_non_model_type(*, type_: type[_T], value: object) -> _T: + model = _create_pydantic_model(type_).validate(value) + return cast(_T, model.__root__) + + def _create_pydantic_model(type_: _T) -> Type[RootModel[_T]]: + return RootModel[type_] # type: ignore + + +class FinalRequestOptionsInput(TypedDict, total=False): + method: Required[str] + url: Required[str] + params: Query + headers: Headers + max_retries: int + timeout: float | Timeout | None + files: HttpxRequestFiles | None + idempotency_key: str + json_data: Body + extra_json: AnyMapping + follow_redirects: bool + + +@final +class FinalRequestOptions(pydantic.BaseModel): + method: str + url: str + params: Query = {} + headers: Union[Headers, NotGiven] = NotGiven() + max_retries: Union[int, NotGiven] = NotGiven() + timeout: Union[float, Timeout, None, NotGiven] = NotGiven() + files: Union[HttpxRequestFiles, None] = None + idempotency_key: Union[str, None] = None + post_parser: Union[Callable[[Any], Any], NotGiven] = NotGiven() + follow_redirects: Union[bool, None] = None + + # It should be noted that we cannot use `json` here as that would override + # a BaseModel method in an incompatible fashion. + json_data: Union[Body, None] = None + extra_json: Union[AnyMapping, None] = None + + if PYDANTIC_V1: + + class Config(pydantic.BaseConfig): # pyright: ignore[reportDeprecated] + arbitrary_types_allowed: bool = True + else: + model_config: ClassVar[ConfigDict] = ConfigDict(arbitrary_types_allowed=True) + + def get_max_retries(self, max_retries: int) -> int: + if isinstance(self.max_retries, NotGiven): + return max_retries + return self.max_retries + + def _strip_raw_response_header(self) -> None: + if not is_given(self.headers): + return + + if self.headers.get(RAW_RESPONSE_HEADER): + self.headers = {**self.headers} + self.headers.pop(RAW_RESPONSE_HEADER) + + # override the `construct` method so that we can run custom transformations. + # this is necessary as we don't want to do any actual runtime type checking + # (which means we can't use validators) but we do want to ensure that `NotGiven` + # values are not present + # + # type ignore required because we're adding explicit types to `**values` + @classmethod + def construct( # type: ignore + cls, + _fields_set: set[str] | None = None, + **values: Unpack[FinalRequestOptionsInput], + ) -> FinalRequestOptions: + kwargs: dict[str, Any] = { + # we unconditionally call `strip_not_given` on any value + # as it will just ignore any non-mapping types + key: strip_not_given(value) + for key, value in values.items() + } + if PYDANTIC_V1: + return cast(FinalRequestOptions, super().construct(_fields_set, **kwargs)) # pyright: ignore[reportDeprecated] + return super().model_construct(_fields_set, **kwargs) + + if not TYPE_CHECKING: + # type checkers incorrectly complain about this assignment + model_construct = construct diff --git a/src/stagehand/_qs.py b/src/stagehand/_qs.py new file mode 100644 index 00000000..ada6fd3f --- /dev/null +++ b/src/stagehand/_qs.py @@ -0,0 +1,150 @@ +from __future__ import annotations + +from typing import Any, List, Tuple, Union, Mapping, TypeVar +from urllib.parse import parse_qs, urlencode +from typing_extensions import Literal, get_args + +from ._types import NotGiven, not_given +from ._utils import flatten + +_T = TypeVar("_T") + + +ArrayFormat = Literal["comma", "repeat", "indices", "brackets"] +NestedFormat = Literal["dots", "brackets"] + +PrimitiveData = Union[str, int, float, bool, None] +# this should be Data = Union[PrimitiveData, "List[Data]", "Tuple[Data]", "Mapping[str, Data]"] +# https://github.com/microsoft/pyright/issues/3555 +Data = Union[PrimitiveData, List[Any], Tuple[Any], "Mapping[str, Any]"] +Params = Mapping[str, Data] + + +class Querystring: + array_format: ArrayFormat + nested_format: NestedFormat + + def __init__( + self, + *, + array_format: ArrayFormat = "repeat", + nested_format: NestedFormat = "brackets", + ) -> None: + self.array_format = array_format + self.nested_format = nested_format + + def parse(self, query: str) -> Mapping[str, object]: + # Note: custom format syntax is not supported yet + return parse_qs(query) + + def stringify( + self, + params: Params, + *, + array_format: ArrayFormat | NotGiven = not_given, + nested_format: NestedFormat | NotGiven = not_given, + ) -> str: + return urlencode( + self.stringify_items( + params, + array_format=array_format, + nested_format=nested_format, + ) + ) + + def stringify_items( + self, + params: Params, + *, + array_format: ArrayFormat | NotGiven = not_given, + nested_format: NestedFormat | NotGiven = not_given, + ) -> list[tuple[str, str]]: + opts = Options( + qs=self, + array_format=array_format, + nested_format=nested_format, + ) + return flatten([self._stringify_item(key, value, opts) for key, value in params.items()]) + + def _stringify_item( + self, + key: str, + value: Data, + opts: Options, + ) -> list[tuple[str, str]]: + if isinstance(value, Mapping): + items: list[tuple[str, str]] = [] + nested_format = opts.nested_format + for subkey, subvalue in value.items(): + items.extend( + self._stringify_item( + # TODO: error if unknown format + f"{key}.{subkey}" if nested_format == "dots" else f"{key}[{subkey}]", + subvalue, + opts, + ) + ) + return items + + if isinstance(value, (list, tuple)): + array_format = opts.array_format + if array_format == "comma": + return [ + ( + key, + ",".join(self._primitive_value_to_str(item) for item in value if item is not None), + ), + ] + elif array_format == "repeat": + items = [] + for item in value: + items.extend(self._stringify_item(key, item, opts)) + return items + elif array_format == "indices": + raise NotImplementedError("The array indices format is not supported yet") + elif array_format == "brackets": + items = [] + key = key + "[]" + for item in value: + items.extend(self._stringify_item(key, item, opts)) + return items + else: + raise NotImplementedError( + f"Unknown array_format value: {array_format}, choose from {', '.join(get_args(ArrayFormat))}" + ) + + serialised = self._primitive_value_to_str(value) + if not serialised: + return [] + return [(key, serialised)] + + def _primitive_value_to_str(self, value: PrimitiveData) -> str: + # copied from httpx + if value is True: + return "true" + elif value is False: + return "false" + elif value is None: + return "" + return str(value) + + +_qs = Querystring() +parse = _qs.parse +stringify = _qs.stringify +stringify_items = _qs.stringify_items + + +class Options: + array_format: ArrayFormat + nested_format: NestedFormat + + def __init__( + self, + qs: Querystring = _qs, + *, + array_format: ArrayFormat | NotGiven = not_given, + nested_format: NestedFormat | NotGiven = not_given, + ) -> None: + self.array_format = qs.array_format if isinstance(array_format, NotGiven) else array_format + self.nested_format = qs.nested_format if isinstance(nested_format, NotGiven) else nested_format diff --git a/src/stagehand/_resource.py b/src/stagehand/_resource.py new file mode 100644 index 00000000..0bdbdf73 --- /dev/null +++ b/src/stagehand/_resource.py @@ -0,0 +1,43 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import time +from typing import TYPE_CHECKING + +import anyio + +if TYPE_CHECKING: + from ._client import Stagehand, AsyncStagehand + + +class SyncAPIResource: + _client: Stagehand + + def __init__(self, client: Stagehand) -> None: + self._client = client + self._get = client.get + self._post = client.post + self._patch = client.patch + self._put = client.put + self._delete = client.delete + self._get_api_list = client.get_api_list + + def _sleep(self, seconds: float) -> None: + time.sleep(seconds) + + +class AsyncAPIResource: + _client: AsyncStagehand + + def __init__(self, client: AsyncStagehand) -> None: + self._client = client + self._get = client.get + self._post = client.post + self._patch = client.patch + self._put = client.put + self._delete = client.delete + self._get_api_list = client.get_api_list + + async def _sleep(self, seconds: float) -> None: + await anyio.sleep(seconds) diff --git a/src/stagehand/_response.py b/src/stagehand/_response.py new file mode 100644 index 00000000..d1003417 --- /dev/null +++ b/src/stagehand/_response.py @@ -0,0 +1,830 @@ +from __future__ import annotations + +import os +import inspect +import logging +import datetime +import functools +from types import TracebackType +from typing import ( + TYPE_CHECKING, + Any, + Union, + Generic, + TypeVar, + Callable, + Iterator, + AsyncIterator, + cast, + overload, +) +from typing_extensions import Awaitable, ParamSpec, override, get_origin + +import anyio +import httpx +import pydantic + +from ._types import NoneType +from ._utils import is_given, extract_type_arg, is_annotated_type, is_type_alias_type, extract_type_var_from_base +from ._models import BaseModel, is_basemodel +from ._constants import RAW_RESPONSE_HEADER, OVERRIDE_CAST_TO_HEADER +from ._streaming import Stream, AsyncStream, is_stream_class_type, extract_stream_chunk_type +from ._exceptions import StagehandError, APIResponseValidationError + +if TYPE_CHECKING: + from ._models import FinalRequestOptions + from ._base_client import BaseClient + + +P = ParamSpec("P") +R = TypeVar("R") +_T = TypeVar("_T") +_APIResponseT = TypeVar("_APIResponseT", bound="APIResponse[Any]") +_AsyncAPIResponseT = TypeVar("_AsyncAPIResponseT", bound="AsyncAPIResponse[Any]") + +log: logging.Logger = logging.getLogger(__name__) + + +class BaseAPIResponse(Generic[R]): + _cast_to: type[R] + _client: BaseClient[Any, Any] + _parsed_by_type: dict[type[Any], Any] + _is_sse_stream: bool + _stream_cls: type[Stream[Any]] | type[AsyncStream[Any]] | None + _options: FinalRequestOptions + + http_response: httpx.Response + + retries_taken: int + """The number of retries made. If no retries happened this will be `0`""" + + def __init__( + self, + *, + raw: httpx.Response, + cast_to: type[R], + client: BaseClient[Any, Any], + stream: bool, + stream_cls: type[Stream[Any]] | type[AsyncStream[Any]] | None, + options: FinalRequestOptions, + retries_taken: int = 0, + ) -> None: + self._cast_to = cast_to + self._client = client + self._parsed_by_type = {} + self._is_sse_stream = stream + self._stream_cls = stream_cls + self._options = options + self.http_response = raw + self.retries_taken = retries_taken + + @property + def headers(self) -> httpx.Headers: + return self.http_response.headers + + @property + def http_request(self) -> httpx.Request: + """Returns the httpx Request instance associated with the current response.""" + return self.http_response.request + + @property + def status_code(self) -> int: + return self.http_response.status_code + + @property + def url(self) -> httpx.URL: + """Returns the URL for which the request was made.""" + return self.http_response.url + + @property + def method(self) -> str: + return self.http_request.method + + @property + def http_version(self) -> str: + return self.http_response.http_version + + @property + def elapsed(self) -> datetime.timedelta: + """The time taken for the complete request/response cycle to complete.""" + return self.http_response.elapsed + + @property + def is_closed(self) -> bool: + """Whether or not the response body has been closed. + + If this is False then there is response data that has not been read yet. + You must either fully consume the response body or call `.close()` + before discarding the response to prevent resource leaks. + """ + return self.http_response.is_closed + + @override + def __repr__(self) -> str: + return ( + f"<{self.__class__.__name__} [{self.status_code} {self.http_response.reason_phrase}] type={self._cast_to}>" + ) + + def _parse(self, *, to: type[_T] | None = None) -> R | _T: + cast_to = to if to is not None else self._cast_to + + # unwrap `TypeAlias('Name', T)` -> `T` + if is_type_alias_type(cast_to): + cast_to = cast_to.__value__ # type: ignore[unreachable] + + # unwrap `Annotated[T, ...]` -> `T` + if cast_to and is_annotated_type(cast_to): + cast_to = extract_type_arg(cast_to, 0) + + origin = get_origin(cast_to) or cast_to + + if self._is_sse_stream: + if to: + if not is_stream_class_type(to): + raise TypeError(f"Expected custom parse type to be a subclass of {Stream} or {AsyncStream}") + + return cast( + _T, + to( + cast_to=extract_stream_chunk_type( + to, + failure_message="Expected custom stream type to be passed with a type argument, e.g. Stream[ChunkType]", + ), + response=self.http_response, + client=cast(Any, self._client), + ), + ) + + if self._stream_cls: + return cast( + R, + self._stream_cls( + cast_to=extract_stream_chunk_type(self._stream_cls), + response=self.http_response, + client=cast(Any, self._client), + ), + ) + + stream_cls = cast("type[Stream[Any]] | type[AsyncStream[Any]] | None", self._client._default_stream_cls) + if stream_cls is None: + raise MissingStreamClassError() + + return cast( + R, + stream_cls( + cast_to=cast_to, + response=self.http_response, + client=cast(Any, self._client), + ), + ) + + if cast_to is NoneType: + return cast(R, None) + + response = self.http_response + if cast_to == str: + return cast(R, response.text) + + if cast_to == bytes: + return cast(R, response.content) + + if cast_to == int: + return cast(R, int(response.text)) + + if cast_to == float: + return cast(R, float(response.text)) + + if cast_to == bool: + return cast(R, response.text.lower() == "true") + + if origin == APIResponse: + raise RuntimeError("Unexpected state - cast_to is `APIResponse`") + + if inspect.isclass(origin) and issubclass(origin, httpx.Response): + # Because of the invariance of our ResponseT TypeVar, users can subclass httpx.Response + # and pass that class to our request functions. We cannot change the variance to be either + # covariant or contravariant as that makes our usage of ResponseT illegal. We could construct + # the response class ourselves but that is something that should be supported directly in httpx + # as it would be easy to incorrectly construct the Response object due to the multitude of arguments. + if cast_to != httpx.Response: + raise ValueError(f"Subclasses of httpx.Response cannot be passed to `cast_to`") + return cast(R, response) + + if ( + inspect.isclass( + origin # pyright: ignore[reportUnknownArgumentType] + ) + and not issubclass(origin, BaseModel) + and issubclass(origin, pydantic.BaseModel) + ): + raise TypeError("Pydantic models must subclass our base model type, e.g. `from stagehand import BaseModel`") + + if ( + cast_to is not object + and not origin is list + and not origin is dict + and not origin is Union + and not issubclass(origin, BaseModel) + ): + raise RuntimeError( + f"Unsupported type, expected {cast_to} to be a subclass of {BaseModel}, {dict}, {list}, {Union}, {NoneType}, {str} or {httpx.Response}." + ) + + # split is required to handle cases where additional information is included + # in the response, e.g. application/json; charset=utf-8 + content_type, *_ = response.headers.get("content-type", "*").split(";") + if not content_type.endswith("json"): + if is_basemodel(cast_to): + try: + data = response.json() + except Exception as exc: + log.debug("Could not read JSON from response data due to %s - %s", type(exc), exc) + else: + return self._client._process_response_data( + data=data, + cast_to=cast_to, # type: ignore + response=response, + ) + + if self._client._strict_response_validation: + raise APIResponseValidationError( + response=response, + message=f"Expected Content-Type response header to be `application/json` but received `{content_type}` instead.", + body=response.text, + ) + + # If the API responds with content that isn't JSON then we just return + # the (decoded) text without performing any parsing so that you can still + # handle the response however you need to. + return response.text # type: ignore + + data = response.json() + + return self._client._process_response_data( + data=data, + cast_to=cast_to, # type: ignore + response=response, + ) + + +class APIResponse(BaseAPIResponse[R]): + @overload + def parse(self, *, to: type[_T]) -> _T: ... + + @overload + def parse(self) -> R: ... + + def parse(self, *, to: type[_T] | None = None) -> R | _T: + """Returns the rich python representation of this response's data. + + For lower-level control, see `.read()`, `.json()`, `.iter_bytes()`. + + You can customise the type that the response is parsed into through + the `to` argument, e.g. + + ```py + from stagehand import BaseModel + + + class MyModel(BaseModel): + foo: str + + + obj = response.parse(to=MyModel) + print(obj.foo) + ``` + + We support parsing: + - `BaseModel` + - `dict` + - `list` + - `Union` + - `str` + - `int` + - `float` + - `httpx.Response` + """ + cache_key = to if to is not None else self._cast_to + cached = self._parsed_by_type.get(cache_key) + if cached is not None: + return cached # type: ignore[no-any-return] + + if not self._is_sse_stream: + self.read() + + parsed = self._parse(to=to) + if is_given(self._options.post_parser): + parsed = self._options.post_parser(parsed) + + self._parsed_by_type[cache_key] = parsed + return parsed + + def read(self) -> bytes: + """Read and return the binary response content.""" + try: + return self.http_response.read() + except httpx.StreamConsumed as exc: + # The default error raised by httpx isn't very + # helpful in our case so we re-raise it with + # a different error message. + raise StreamAlreadyConsumed() from exc + + def text(self) -> str: + """Read and decode the response content into a string.""" + self.read() + return self.http_response.text + + def json(self) -> object: + """Read and decode the JSON response content.""" + self.read() + return self.http_response.json() + + def close(self) -> None: + """Close the response and release the connection. + + Automatically called if the response body is read to completion. + """ + self.http_response.close() + + def iter_bytes(self, chunk_size: int | None = None) -> Iterator[bytes]: + """ + A byte-iterator over the decoded response content. + + This automatically handles gzip, deflate and brotli encoded responses. + """ + for chunk in self.http_response.iter_bytes(chunk_size): + yield chunk + + def iter_text(self, chunk_size: int | None = None) -> Iterator[str]: + """A str-iterator over the decoded response content + that handles both gzip, deflate, etc but also detects the content's + string encoding. + """ + for chunk in self.http_response.iter_text(chunk_size): + yield chunk + + def iter_lines(self) -> Iterator[str]: + """Like `iter_text()` but will only yield chunks for each line""" + for chunk in self.http_response.iter_lines(): + yield chunk + + +class AsyncAPIResponse(BaseAPIResponse[R]): + @overload + async def parse(self, *, to: type[_T]) -> _T: ... + + @overload + async def parse(self) -> R: ... + + async def parse(self, *, to: type[_T] | None = None) -> R | _T: + """Returns the rich python representation of this response's data. + + For lower-level control, see `.read()`, `.json()`, `.iter_bytes()`. + + You can customise the type that the response is parsed into through + the `to` argument, e.g. + + ```py + from stagehand import BaseModel + + + class MyModel(BaseModel): + foo: str + + + obj = response.parse(to=MyModel) + print(obj.foo) + ``` + + We support parsing: + - `BaseModel` + - `dict` + - `list` + - `Union` + - `str` + - `httpx.Response` + """ + cache_key = to if to is not None else self._cast_to + cached = self._parsed_by_type.get(cache_key) + if cached is not None: + return cached # type: ignore[no-any-return] + + if not self._is_sse_stream: + await self.read() + + parsed = self._parse(to=to) + if is_given(self._options.post_parser): + parsed = self._options.post_parser(parsed) + + self._parsed_by_type[cache_key] = parsed + return parsed + + async def read(self) -> bytes: + """Read and return the binary response content.""" + try: + return await self.http_response.aread() + except httpx.StreamConsumed as exc: + # the default error raised by httpx isn't very + # helpful in our case so we re-raise it with + # a different error message + raise StreamAlreadyConsumed() from exc + + async def text(self) -> str: + """Read and decode the response content into a string.""" + await self.read() + return self.http_response.text + + async def json(self) -> object: + """Read and decode the JSON response content.""" + await self.read() + return self.http_response.json() + + async def close(self) -> None: + """Close the response and release the connection. + + Automatically called if the response body is read to completion. + """ + await self.http_response.aclose() + + async def iter_bytes(self, chunk_size: int | None = None) -> AsyncIterator[bytes]: + """ + A byte-iterator over the decoded response content. + + This automatically handles gzip, deflate and brotli encoded responses. + """ + async for chunk in self.http_response.aiter_bytes(chunk_size): + yield chunk + + async def iter_text(self, chunk_size: int | None = None) -> AsyncIterator[str]: + """A str-iterator over the decoded response content + that handles both gzip, deflate, etc but also detects the content's + string encoding. + """ + async for chunk in self.http_response.aiter_text(chunk_size): + yield chunk + + async def iter_lines(self) -> AsyncIterator[str]: + """Like `iter_text()` but will only yield chunks for each line""" + async for chunk in self.http_response.aiter_lines(): + yield chunk + + +class BinaryAPIResponse(APIResponse[bytes]): + """Subclass of APIResponse providing helpers for dealing with binary data. + + Note: If you want to stream the response data instead of eagerly reading it + all at once then you should use `.with_streaming_response` when making + the API request, e.g. `.with_streaming_response.get_binary_response()` + """ + + def write_to_file( + self, + file: str | os.PathLike[str], + ) -> None: + """Write the output to the given file. + + Accepts a filename or any path-like object, e.g. pathlib.Path + + Note: if you want to stream the data to the file instead of writing + all at once then you should use `.with_streaming_response` when making + the API request, e.g. `.with_streaming_response.get_binary_response()` + """ + with open(file, mode="wb") as f: + for data in self.iter_bytes(): + f.write(data) + + +class AsyncBinaryAPIResponse(AsyncAPIResponse[bytes]): + """Subclass of APIResponse providing helpers for dealing with binary data. + + Note: If you want to stream the response data instead of eagerly reading it + all at once then you should use `.with_streaming_response` when making + the API request, e.g. `.with_streaming_response.get_binary_response()` + """ + + async def write_to_file( + self, + file: str | os.PathLike[str], + ) -> None: + """Write the output to the given file. + + Accepts a filename or any path-like object, e.g. pathlib.Path + + Note: if you want to stream the data to the file instead of writing + all at once then you should use `.with_streaming_response` when making + the API request, e.g. `.with_streaming_response.get_binary_response()` + """ + path = anyio.Path(file) + async with await path.open(mode="wb") as f: + async for data in self.iter_bytes(): + await f.write(data) + + +class StreamedBinaryAPIResponse(APIResponse[bytes]): + def stream_to_file( + self, + file: str | os.PathLike[str], + *, + chunk_size: int | None = None, + ) -> None: + """Streams the output to the given file. + + Accepts a filename or any path-like object, e.g. pathlib.Path + """ + with open(file, mode="wb") as f: + for data in self.iter_bytes(chunk_size): + f.write(data) + + +class AsyncStreamedBinaryAPIResponse(AsyncAPIResponse[bytes]): + async def stream_to_file( + self, + file: str | os.PathLike[str], + *, + chunk_size: int | None = None, + ) -> None: + """Streams the output to the given file. + + Accepts a filename or any path-like object, e.g. pathlib.Path + """ + path = anyio.Path(file) + async with await path.open(mode="wb") as f: + async for data in self.iter_bytes(chunk_size): + await f.write(data) + + +class MissingStreamClassError(TypeError): + def __init__(self) -> None: + super().__init__( + "The `stream` argument was set to `True` but the `stream_cls` argument was not given. See `stagehand._streaming` for reference", + ) + + +class StreamAlreadyConsumed(StagehandError): + """ + Attempted to read or stream content, but the content has already + been streamed. + + This can happen if you use a method like `.iter_lines()` and then attempt + to read th entire response body afterwards, e.g. + + ```py + response = await client.post(...) + async for line in response.iter_lines(): + ... # do something with `line` + + content = await response.read() + # ^ error + ``` + + If you want this behaviour you'll need to either manually accumulate the response + content or call `await response.read()` before iterating over the stream. + """ + + def __init__(self) -> None: + message = ( + "Attempted to read or stream some content, but the content has " + "already been streamed. " + "This could be due to attempting to stream the response " + "content more than once." + "\n\n" + "You can fix this by manually accumulating the response content while streaming " + "or by calling `.read()` before starting to stream." + ) + super().__init__(message) + + +class ResponseContextManager(Generic[_APIResponseT]): + """Context manager for ensuring that a request is not made + until it is entered and that the response will always be closed + when the context manager exits + """ + + def __init__(self, request_func: Callable[[], _APIResponseT]) -> None: + self._request_func = request_func + self.__response: _APIResponseT | None = None + + def __enter__(self) -> _APIResponseT: + self.__response = self._request_func() + return self.__response + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc: BaseException | None, + exc_tb: TracebackType | None, + ) -> None: + if self.__response is not None: + self.__response.close() + + +class AsyncResponseContextManager(Generic[_AsyncAPIResponseT]): + """Context manager for ensuring that a request is not made + until it is entered and that the response will always be closed + when the context manager exits + """ + + def __init__(self, api_request: Awaitable[_AsyncAPIResponseT]) -> None: + self._api_request = api_request + self.__response: _AsyncAPIResponseT | None = None + + async def __aenter__(self) -> _AsyncAPIResponseT: + self.__response = await self._api_request + return self.__response + + async def __aexit__( + self, + exc_type: type[BaseException] | None, + exc: BaseException | None, + exc_tb: TracebackType | None, + ) -> None: + if self.__response is not None: + await self.__response.close() + + +def to_streamed_response_wrapper(func: Callable[P, R]) -> Callable[P, ResponseContextManager[APIResponse[R]]]: + """Higher order function that takes one of our bound API methods and wraps it + to support streaming and returning the raw `APIResponse` object directly. + """ + + @functools.wraps(func) + def wrapped(*args: P.args, **kwargs: P.kwargs) -> ResponseContextManager[APIResponse[R]]: + extra_headers: dict[str, str] = {**(cast(Any, kwargs.get("extra_headers")) or {})} + extra_headers[RAW_RESPONSE_HEADER] = "stream" + + kwargs["extra_headers"] = extra_headers + + make_request = functools.partial(func, *args, **kwargs) + + return ResponseContextManager(cast(Callable[[], APIResponse[R]], make_request)) + + return wrapped + + +def async_to_streamed_response_wrapper( + func: Callable[P, Awaitable[R]], +) -> Callable[P, AsyncResponseContextManager[AsyncAPIResponse[R]]]: + """Higher order function that takes one of our bound API methods and wraps it + to support streaming and returning the raw `APIResponse` object directly. + """ + + @functools.wraps(func) + def wrapped(*args: P.args, **kwargs: P.kwargs) -> AsyncResponseContextManager[AsyncAPIResponse[R]]: + extra_headers: dict[str, str] = {**(cast(Any, kwargs.get("extra_headers")) or {})} + extra_headers[RAW_RESPONSE_HEADER] = "stream" + + kwargs["extra_headers"] = extra_headers + + make_request = func(*args, **kwargs) + + return AsyncResponseContextManager(cast(Awaitable[AsyncAPIResponse[R]], make_request)) + + return wrapped + + +def to_custom_streamed_response_wrapper( + func: Callable[P, object], + response_cls: type[_APIResponseT], +) -> Callable[P, ResponseContextManager[_APIResponseT]]: + """Higher order function that takes one of our bound API methods and an `APIResponse` class + and wraps the method to support streaming and returning the given response class directly. + + Note: the given `response_cls` *must* be concrete, e.g. `class BinaryAPIResponse(APIResponse[bytes])` + """ + + @functools.wraps(func) + def wrapped(*args: P.args, **kwargs: P.kwargs) -> ResponseContextManager[_APIResponseT]: + extra_headers: dict[str, Any] = {**(cast(Any, kwargs.get("extra_headers")) or {})} + extra_headers[RAW_RESPONSE_HEADER] = "stream" + extra_headers[OVERRIDE_CAST_TO_HEADER] = response_cls + + kwargs["extra_headers"] = extra_headers + + make_request = functools.partial(func, *args, **kwargs) + + return ResponseContextManager(cast(Callable[[], _APIResponseT], make_request)) + + return wrapped + + +def async_to_custom_streamed_response_wrapper( + func: Callable[P, Awaitable[object]], + response_cls: type[_AsyncAPIResponseT], +) -> Callable[P, AsyncResponseContextManager[_AsyncAPIResponseT]]: + """Higher order function that takes one of our bound API methods and an `APIResponse` class + and wraps the method to support streaming and returning the given response class directly. + + Note: the given `response_cls` *must* be concrete, e.g. `class BinaryAPIResponse(APIResponse[bytes])` + """ + + @functools.wraps(func) + def wrapped(*args: P.args, **kwargs: P.kwargs) -> AsyncResponseContextManager[_AsyncAPIResponseT]: + extra_headers: dict[str, Any] = {**(cast(Any, kwargs.get("extra_headers")) or {})} + extra_headers[RAW_RESPONSE_HEADER] = "stream" + extra_headers[OVERRIDE_CAST_TO_HEADER] = response_cls + + kwargs["extra_headers"] = extra_headers + + make_request = func(*args, **kwargs) + + return AsyncResponseContextManager(cast(Awaitable[_AsyncAPIResponseT], make_request)) + + return wrapped + + +def to_raw_response_wrapper(func: Callable[P, R]) -> Callable[P, APIResponse[R]]: + """Higher order function that takes one of our bound API methods and wraps it + to support returning the raw `APIResponse` object directly. + """ + + @functools.wraps(func) + def wrapped(*args: P.args, **kwargs: P.kwargs) -> APIResponse[R]: + extra_headers: dict[str, str] = {**(cast(Any, kwargs.get("extra_headers")) or {})} + extra_headers[RAW_RESPONSE_HEADER] = "raw" + + kwargs["extra_headers"] = extra_headers + + return cast(APIResponse[R], func(*args, **kwargs)) + + return wrapped + + +def async_to_raw_response_wrapper(func: Callable[P, Awaitable[R]]) -> Callable[P, Awaitable[AsyncAPIResponse[R]]]: + """Higher order function that takes one of our bound API methods and wraps it + to support returning the raw `APIResponse` object directly. + """ + + @functools.wraps(func) + async def wrapped(*args: P.args, **kwargs: P.kwargs) -> AsyncAPIResponse[R]: + extra_headers: dict[str, str] = {**(cast(Any, kwargs.get("extra_headers")) or {})} + extra_headers[RAW_RESPONSE_HEADER] = "raw" + + kwargs["extra_headers"] = extra_headers + + return cast(AsyncAPIResponse[R], await func(*args, **kwargs)) + + return wrapped + + +def to_custom_raw_response_wrapper( + func: Callable[P, object], + response_cls: type[_APIResponseT], +) -> Callable[P, _APIResponseT]: + """Higher order function that takes one of our bound API methods and an `APIResponse` class + and wraps the method to support returning the given response class directly. + + Note: the given `response_cls` *must* be concrete, e.g. `class BinaryAPIResponse(APIResponse[bytes])` + """ + + @functools.wraps(func) + def wrapped(*args: P.args, **kwargs: P.kwargs) -> _APIResponseT: + extra_headers: dict[str, Any] = {**(cast(Any, kwargs.get("extra_headers")) or {})} + extra_headers[RAW_RESPONSE_HEADER] = "raw" + extra_headers[OVERRIDE_CAST_TO_HEADER] = response_cls + + kwargs["extra_headers"] = extra_headers + + return cast(_APIResponseT, func(*args, **kwargs)) + + return wrapped + + +def async_to_custom_raw_response_wrapper( + func: Callable[P, Awaitable[object]], + response_cls: type[_AsyncAPIResponseT], +) -> Callable[P, Awaitable[_AsyncAPIResponseT]]: + """Higher order function that takes one of our bound API methods and an `APIResponse` class + and wraps the method to support returning the given response class directly. + + Note: the given `response_cls` *must* be concrete, e.g. `class BinaryAPIResponse(APIResponse[bytes])` + """ + + @functools.wraps(func) + def wrapped(*args: P.args, **kwargs: P.kwargs) -> Awaitable[_AsyncAPIResponseT]: + extra_headers: dict[str, Any] = {**(cast(Any, kwargs.get("extra_headers")) or {})} + extra_headers[RAW_RESPONSE_HEADER] = "raw" + extra_headers[OVERRIDE_CAST_TO_HEADER] = response_cls + + kwargs["extra_headers"] = extra_headers + + return cast(Awaitable[_AsyncAPIResponseT], func(*args, **kwargs)) + + return wrapped + + +def extract_response_type(typ: type[BaseAPIResponse[Any]]) -> type: + """Given a type like `APIResponse[T]`, returns the generic type variable `T`. + + This also handles the case where a concrete subclass is given, e.g. + ```py + class MyResponse(APIResponse[bytes]): + ... + + extract_response_type(MyResponse) -> bytes + ``` + """ + return extract_type_var_from_base( + typ, + generic_bases=cast("tuple[type, ...]", (BaseAPIResponse, APIResponse, AsyncAPIResponse)), + index=0, + ) diff --git a/src/stagehand/_streaming.py b/src/stagehand/_streaming.py new file mode 100644 index 00000000..69a9442b --- /dev/null +++ b/src/stagehand/_streaming.py @@ -0,0 +1,333 @@ +# Note: initially copied from https://github.com/florimondmanca/httpx-sse/blob/master/src/httpx_sse/_decoders.py +from __future__ import annotations + +import json +import inspect +from types import TracebackType +from typing import TYPE_CHECKING, Any, Generic, TypeVar, Iterator, AsyncIterator, cast +from typing_extensions import Self, Protocol, TypeGuard, override, get_origin, runtime_checkable + +import httpx + +from ._utils import extract_type_var_from_base + +if TYPE_CHECKING: + from ._client import Stagehand, AsyncStagehand + + +_T = TypeVar("_T") + + +class Stream(Generic[_T]): + """Provides the core interface to iterate over a synchronous stream response.""" + + response: httpx.Response + + _decoder: SSEBytesDecoder + + def __init__( + self, + *, + cast_to: type[_T], + response: httpx.Response, + client: Stagehand, + ) -> None: + self.response = response + self._cast_to = cast_to + self._client = client + self._decoder = client._make_sse_decoder() + self._iterator = self.__stream__() + + def __next__(self) -> _T: + return self._iterator.__next__() + + def __iter__(self) -> Iterator[_T]: + for item in self._iterator: + yield item + + def _iter_events(self) -> Iterator[ServerSentEvent]: + yield from self._decoder.iter_bytes(self.response.iter_bytes()) + + def __stream__(self) -> Iterator[_T]: + cast_to = cast(Any, self._cast_to) + response = self.response + process_data = self._client._process_response_data + iterator = self._iter_events() + + try: + for sse in iterator: + yield process_data(data=sse.json(), cast_to=cast_to, response=response) + finally: + # Ensure the response is closed even if the consumer doesn't read all data + response.close() + + def __enter__(self) -> Self: + return self + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc: BaseException | None, + exc_tb: TracebackType | None, + ) -> None: + self.close() + + def close(self) -> None: + """ + Close the response and release the connection. + + Automatically called if the response body is read to completion. + """ + self.response.close() + + +class AsyncStream(Generic[_T]): + """Provides the core interface to iterate over an asynchronous stream response.""" + + response: httpx.Response + + _decoder: SSEDecoder | SSEBytesDecoder + + def __init__( + self, + *, + cast_to: type[_T], + response: httpx.Response, + client: AsyncStagehand, + ) -> None: + self.response = response + self._cast_to = cast_to + self._client = client + self._decoder = client._make_sse_decoder() + self._iterator = self.__stream__() + + async def __anext__(self) -> _T: + return await self._iterator.__anext__() + + async def __aiter__(self) -> AsyncIterator[_T]: + async for item in self._iterator: + yield item + + async def _iter_events(self) -> AsyncIterator[ServerSentEvent]: + async for sse in self._decoder.aiter_bytes(self.response.aiter_bytes()): + yield sse + + async def __stream__(self) -> AsyncIterator[_T]: + cast_to = cast(Any, self._cast_to) + response = self.response + process_data = self._client._process_response_data + iterator = self._iter_events() + + try: + async for sse in iterator: + yield process_data(data=sse.json(), cast_to=cast_to, response=response) + finally: + # Ensure the response is closed even if the consumer doesn't read all data + await response.aclose() + + async def __aenter__(self) -> Self: + return self + + async def __aexit__( + self, + exc_type: type[BaseException] | None, + exc: BaseException | None, + exc_tb: TracebackType | None, + ) -> None: + await self.close() + + async def close(self) -> None: + """ + Close the response and release the connection. + + Automatically called if the response body is read to completion. + """ + await self.response.aclose() + + +class ServerSentEvent: + def __init__( + self, + *, + event: str | None = None, + data: str | None = None, + id: str | None = None, + retry: int | None = None, + ) -> None: + if data is None: + data = "" + + self._id = id + self._data = data + self._event = event or None + self._retry = retry + + @property + def event(self) -> str | None: + return self._event + + @property + def id(self) -> str | None: + return self._id + + @property + def retry(self) -> int | None: + return self._retry + + @property + def data(self) -> str: + return self._data + + def json(self) -> Any: + return json.loads(self.data) + + @override + def __repr__(self) -> str: + return f"ServerSentEvent(event={self.event}, data={self.data}, id={self.id}, retry={self.retry})" + + +class SSEDecoder: + _data: list[str] + _event: str | None + _retry: int | None + _last_event_id: str | None + + def __init__(self) -> None: + self._event = None + self._data = [] + self._last_event_id = None + self._retry = None + + def iter_bytes(self, iterator: Iterator[bytes]) -> Iterator[ServerSentEvent]: + """Given an iterator that yields raw binary data, iterate over it & yield every event encountered""" + for chunk in self._iter_chunks(iterator): + # Split before decoding so splitlines() only uses \r and \n + for raw_line in chunk.splitlines(): + line = raw_line.decode("utf-8") + sse = self.decode(line) + if sse: + yield sse + + def _iter_chunks(self, iterator: Iterator[bytes]) -> Iterator[bytes]: + """Given an iterator that yields raw binary data, iterate over it and yield individual SSE chunks""" + data = b"" + for chunk in iterator: + for line in chunk.splitlines(keepends=True): + data += line + if data.endswith((b"\r\r", b"\n\n", b"\r\n\r\n")): + yield data + data = b"" + if data: + yield data + + async def aiter_bytes(self, iterator: AsyncIterator[bytes]) -> AsyncIterator[ServerSentEvent]: + """Given an iterator that yields raw binary data, iterate over it & yield every event encountered""" + async for chunk in self._aiter_chunks(iterator): + # Split before decoding so splitlines() only uses \r and \n + for raw_line in chunk.splitlines(): + line = raw_line.decode("utf-8") + sse = self.decode(line) + if sse: + yield sse + + async def _aiter_chunks(self, iterator: AsyncIterator[bytes]) -> AsyncIterator[bytes]: + """Given an iterator that yields raw binary data, iterate over it and yield individual SSE chunks""" + data = b"" + async for chunk in iterator: + for line in chunk.splitlines(keepends=True): + data += line + if data.endswith((b"\r\r", b"\n\n", b"\r\n\r\n")): + yield data + data = b"" + if data: + yield data + + def decode(self, line: str) -> ServerSentEvent | None: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = None + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None + + +@runtime_checkable +class SSEBytesDecoder(Protocol): + def iter_bytes(self, iterator: Iterator[bytes]) -> Iterator[ServerSentEvent]: + """Given an iterator that yields raw binary data, iterate over it & yield every event encountered""" + ... + + def aiter_bytes(self, iterator: AsyncIterator[bytes]) -> AsyncIterator[ServerSentEvent]: + """Given an async iterator that yields raw binary data, iterate over it & yield every event encountered""" + ... + + +def is_stream_class_type(typ: type) -> TypeGuard[type[Stream[object]] | type[AsyncStream[object]]]: + """TypeGuard for determining whether or not the given type is a subclass of `Stream` / `AsyncStream`""" + origin = get_origin(typ) or typ + return inspect.isclass(origin) and issubclass(origin, (Stream, AsyncStream)) + + +def extract_stream_chunk_type( + stream_cls: type, + *, + failure_message: str | None = None, +) -> type: + """Given a type like `Stream[T]`, returns the generic type variable `T`. + + This also handles the case where a concrete subclass is given, e.g. + ```py + class MyStream(Stream[bytes]): + ... + + extract_stream_chunk_type(MyStream) -> bytes + ``` + """ + from ._base_client import Stream, AsyncStream + + return extract_type_var_from_base( + stream_cls, + index=0, + generic_bases=cast("tuple[type, ...]", (Stream, AsyncStream)), + failure_message=failure_message, + ) diff --git a/src/stagehand/_types.py b/src/stagehand/_types.py new file mode 100644 index 00000000..c2ce0d7a --- /dev/null +++ b/src/stagehand/_types.py @@ -0,0 +1,261 @@ +from __future__ import annotations + +from os import PathLike +from typing import ( + IO, + TYPE_CHECKING, + Any, + Dict, + List, + Type, + Tuple, + Union, + Mapping, + TypeVar, + Callable, + Iterator, + Optional, + Sequence, +) +from typing_extensions import ( + Set, + Literal, + Protocol, + TypeAlias, + TypedDict, + SupportsIndex, + overload, + override, + runtime_checkable, +) + +import httpx +import pydantic +from httpx import URL, Proxy, Timeout, Response, BaseTransport, AsyncBaseTransport + +if TYPE_CHECKING: + from ._models import BaseModel + from ._response import APIResponse, AsyncAPIResponse + +Transport = BaseTransport +AsyncTransport = AsyncBaseTransport +Query = Mapping[str, object] +Body = object +AnyMapping = Mapping[str, object] +ModelT = TypeVar("ModelT", bound=pydantic.BaseModel) +_T = TypeVar("_T") + + +# Approximates httpx internal ProxiesTypes and RequestFiles types +# while adding support for `PathLike` instances +ProxiesDict = Dict["str | URL", Union[None, str, URL, Proxy]] +ProxiesTypes = Union[str, Proxy, ProxiesDict] +if TYPE_CHECKING: + Base64FileInput = Union[IO[bytes], PathLike[str]] + FileContent = Union[IO[bytes], bytes, PathLike[str]] +else: + Base64FileInput = Union[IO[bytes], PathLike] + FileContent = Union[IO[bytes], bytes, PathLike] # PathLike is not subscriptable in Python 3.8. +FileTypes = Union[ + # file (or bytes) + FileContent, + # (filename, file (or bytes)) + Tuple[Optional[str], FileContent], + # (filename, file (or bytes), content_type) + Tuple[Optional[str], FileContent, Optional[str]], + # (filename, file (or bytes), content_type, headers) + Tuple[Optional[str], FileContent, Optional[str], Mapping[str, str]], +] +RequestFiles = Union[Mapping[str, FileTypes], Sequence[Tuple[str, FileTypes]]] + +# duplicate of the above but without our custom file support +HttpxFileContent = Union[IO[bytes], bytes] +HttpxFileTypes = Union[ + # file (or bytes) + HttpxFileContent, + # (filename, file (or bytes)) + Tuple[Optional[str], HttpxFileContent], + # (filename, file (or bytes), content_type) + Tuple[Optional[str], HttpxFileContent, Optional[str]], + # (filename, file (or bytes), content_type, headers) + Tuple[Optional[str], HttpxFileContent, Optional[str], Mapping[str, str]], +] +HttpxRequestFiles = Union[Mapping[str, HttpxFileTypes], Sequence[Tuple[str, HttpxFileTypes]]] + +# Workaround to support (cast_to: Type[ResponseT]) -> ResponseT +# where ResponseT includes `None`. In order to support directly +# passing `None`, overloads would have to be defined for every +# method that uses `ResponseT` which would lead to an unacceptable +# amount of code duplication and make it unreadable. See _base_client.py +# for example usage. +# +# This unfortunately means that you will either have +# to import this type and pass it explicitly: +# +# from stagehand import NoneType +# client.get('/foo', cast_to=NoneType) +# +# or build it yourself: +# +# client.get('/foo', cast_to=type(None)) +if TYPE_CHECKING: + NoneType: Type[None] +else: + NoneType = type(None) + + +class RequestOptions(TypedDict, total=False): + headers: Headers + max_retries: int + timeout: float | Timeout | None + params: Query + extra_json: AnyMapping + idempotency_key: str + follow_redirects: bool + + +# Sentinel class used until PEP 0661 is accepted +class NotGiven: + """ + For parameters with a meaningful None value, we need to distinguish between + the user explicitly passing None, and the user not passing the parameter at + all. + + User code shouldn't need to use not_given directly. + + For example: + + ```py + def create(timeout: Timeout | None | NotGiven = not_given): ... + + + create(timeout=1) # 1s timeout + create(timeout=None) # No timeout + create() # Default timeout behavior + ``` + """ + + def __bool__(self) -> Literal[False]: + return False + + @override + def __repr__(self) -> str: + return "NOT_GIVEN" + + +not_given = NotGiven() +# for backwards compatibility: +NOT_GIVEN = NotGiven() + + +class Omit: + """ + To explicitly omit something from being sent in a request, use `omit`. + + ```py + # as the default `Content-Type` header is `application/json` that will be sent + client.post("/upload/files", files={"file": b"my raw file content"}) + + # you can't explicitly override the header as it has to be dynamically generated + # to look something like: 'multipart/form-data; boundary=0d8382fcf5f8c3be01ca2e11002d2983' + client.post(..., headers={"Content-Type": "multipart/form-data"}) + + # instead you can remove the default `application/json` header by passing omit + client.post(..., headers={"Content-Type": omit}) + ``` + """ + + def __bool__(self) -> Literal[False]: + return False + + +omit = Omit() + + +@runtime_checkable +class ModelBuilderProtocol(Protocol): + @classmethod + def build( + cls: type[_T], + *, + response: Response, + data: object, + ) -> _T: ... + + +Headers = Mapping[str, Union[str, Omit]] + + +class HeadersLikeProtocol(Protocol): + def get(self, __key: str) -> str | None: ... + + +HeadersLike = Union[Headers, HeadersLikeProtocol] + +ResponseT = TypeVar( + "ResponseT", + bound=Union[ + object, + str, + None, + "BaseModel", + List[Any], + Dict[str, Any], + Response, + ModelBuilderProtocol, + "APIResponse[Any]", + "AsyncAPIResponse[Any]", + ], +) + +StrBytesIntFloat = Union[str, bytes, int, float] + +# Note: copied from Pydantic +# https://github.com/pydantic/pydantic/blob/6f31f8f68ef011f84357330186f603ff295312fd/pydantic/main.py#L79 +IncEx: TypeAlias = Union[Set[int], Set[str], Mapping[int, Union["IncEx", bool]], Mapping[str, Union["IncEx", bool]]] + +PostParser = Callable[[Any], Any] + + +@runtime_checkable +class InheritsGeneric(Protocol): + """Represents a type that has inherited from `Generic` + + The `__orig_bases__` property can be used to determine the resolved + type variable for a given base class. + """ + + __orig_bases__: tuple[_GenericAlias] + + +class _GenericAlias(Protocol): + __origin__: type[object] + + +class HttpxSendArgs(TypedDict, total=False): + auth: httpx.Auth + follow_redirects: bool + + +_T_co = TypeVar("_T_co", covariant=True) + + +if TYPE_CHECKING: + # This works because str.__contains__ does not accept object (either in typeshed or at runtime) + # https://github.com/hauntsaninja/useful_types/blob/5e9710f3875107d068e7679fd7fec9cfab0eff3b/useful_types/__init__.py#L285 + # + # Note: index() and count() methods are intentionally omitted to allow pyright to properly + # infer TypedDict types when dict literals are used in lists assigned to SequenceNotStr. + class SequenceNotStr(Protocol[_T_co]): + @overload + def __getitem__(self, index: SupportsIndex, /) -> _T_co: ... + @overload + def __getitem__(self, index: slice, /) -> Sequence[_T_co]: ... + def __contains__(self, value: object, /) -> bool: ... + def __len__(self) -> int: ... + def __iter__(self) -> Iterator[_T_co]: ... + def __reversed__(self) -> Iterator[_T_co]: ... +else: + # just point this to a normal `Sequence` at runtime to avoid having to special case + # deserializing our custom sequence type + SequenceNotStr = Sequence diff --git a/src/stagehand/_utils/__init__.py b/src/stagehand/_utils/__init__.py new file mode 100644 index 00000000..dc64e29a --- /dev/null +++ b/src/stagehand/_utils/__init__.py @@ -0,0 +1,64 @@ +from ._sync import asyncify as asyncify +from ._proxy import LazyProxy as LazyProxy +from ._utils import ( + flatten as flatten, + is_dict as is_dict, + is_list as is_list, + is_given as is_given, + is_tuple as is_tuple, + json_safe as json_safe, + lru_cache as lru_cache, + is_mapping as is_mapping, + is_tuple_t as is_tuple_t, + is_iterable as is_iterable, + is_sequence as is_sequence, + coerce_float as coerce_float, + is_mapping_t as is_mapping_t, + removeprefix as removeprefix, + removesuffix as removesuffix, + extract_files as extract_files, + is_sequence_t as is_sequence_t, + required_args as required_args, + coerce_boolean as coerce_boolean, + coerce_integer as coerce_integer, + file_from_path as file_from_path, + strip_not_given as strip_not_given, + deepcopy_minimal as deepcopy_minimal, + get_async_library as get_async_library, + maybe_coerce_float as maybe_coerce_float, + get_required_header as get_required_header, + maybe_coerce_boolean as maybe_coerce_boolean, + maybe_coerce_integer as maybe_coerce_integer, +) +from ._compat import ( + get_args as get_args, + is_union as is_union, + get_origin as get_origin, + is_typeddict as is_typeddict, + is_literal_type as is_literal_type, +) +from ._typing import ( + is_list_type as is_list_type, + is_union_type as is_union_type, + extract_type_arg as extract_type_arg, + is_iterable_type as is_iterable_type, + is_required_type as is_required_type, + is_sequence_type as is_sequence_type, + is_annotated_type as is_annotated_type, + is_type_alias_type as is_type_alias_type, + strip_annotated_type as strip_annotated_type, + extract_type_var_from_base as extract_type_var_from_base, +) +from ._streams import consume_sync_iterator as consume_sync_iterator, consume_async_iterator as consume_async_iterator +from ._transform import ( + PropertyInfo as PropertyInfo, + transform as transform, + async_transform as async_transform, + maybe_transform as maybe_transform, + async_maybe_transform as async_maybe_transform, +) +from ._reflection import ( + function_has_argument as function_has_argument, + assert_signatures_in_sync as assert_signatures_in_sync, +) +from ._datetime_parse import parse_date as parse_date, parse_datetime as parse_datetime diff --git a/src/stagehand/_utils/_compat.py b/src/stagehand/_utils/_compat.py new file mode 100644 index 00000000..dd703233 --- /dev/null +++ b/src/stagehand/_utils/_compat.py @@ -0,0 +1,45 @@ +from __future__ import annotations + +import sys +import typing_extensions +from typing import Any, Type, Union, Literal, Optional +from datetime import date, datetime +from typing_extensions import get_args as _get_args, get_origin as _get_origin + +from .._types import StrBytesIntFloat +from ._datetime_parse import parse_date as _parse_date, parse_datetime as _parse_datetime + +_LITERAL_TYPES = {Literal, typing_extensions.Literal} + + +def get_args(tp: type[Any]) -> tuple[Any, ...]: + return _get_args(tp) + + +def get_origin(tp: type[Any]) -> type[Any] | None: + return _get_origin(tp) + + +def is_union(tp: Optional[Type[Any]]) -> bool: + if sys.version_info < (3, 10): + return tp is Union # type: ignore[comparison-overlap] + else: + import types + + return tp is Union or tp is types.UnionType + + +def is_typeddict(tp: Type[Any]) -> bool: + return typing_extensions.is_typeddict(tp) + + +def is_literal_type(tp: Type[Any]) -> bool: + return get_origin(tp) in _LITERAL_TYPES + + +def parse_date(value: Union[date, StrBytesIntFloat]) -> date: + return _parse_date(value) + + +def parse_datetime(value: Union[datetime, StrBytesIntFloat]) -> datetime: + return _parse_datetime(value) diff --git a/src/stagehand/_utils/_datetime_parse.py b/src/stagehand/_utils/_datetime_parse.py new file mode 100644 index 00000000..7cb9d9e6 --- /dev/null +++ b/src/stagehand/_utils/_datetime_parse.py @@ -0,0 +1,136 @@ +""" +This file contains code from https://github.com/pydantic/pydantic/blob/main/pydantic/v1/datetime_parse.py +without the Pydantic v1 specific errors. +""" + +from __future__ import annotations + +import re +from typing import Dict, Union, Optional +from datetime import date, datetime, timezone, timedelta + +from .._types import StrBytesIntFloat + +date_expr = r"(?P\d{4})-(?P\d{1,2})-(?P\d{1,2})" +time_expr = ( + r"(?P\d{1,2}):(?P\d{1,2})" + r"(?::(?P\d{1,2})(?:\.(?P\d{1,6})\d{0,6})?)?" + r"(?PZ|[+-]\d{2}(?::?\d{2})?)?$" +) + +date_re = re.compile(f"{date_expr}$") +datetime_re = re.compile(f"{date_expr}[T ]{time_expr}") + + +EPOCH = datetime(1970, 1, 1) +# if greater than this, the number is in ms, if less than or equal it's in seconds +# (in seconds this is 11th October 2603, in ms it's 20th August 1970) +MS_WATERSHED = int(2e10) +# slightly more than datetime.max in ns - (datetime.max - EPOCH).total_seconds() * 1e9 +MAX_NUMBER = int(3e20) + + +def _get_numeric(value: StrBytesIntFloat, native_expected_type: str) -> Union[None, int, float]: + if isinstance(value, (int, float)): + return value + try: + return float(value) + except ValueError: + return None + except TypeError: + raise TypeError(f"invalid type; expected {native_expected_type}, string, bytes, int or float") from None + + +def _from_unix_seconds(seconds: Union[int, float]) -> datetime: + if seconds > MAX_NUMBER: + return datetime.max + elif seconds < -MAX_NUMBER: + return datetime.min + + while abs(seconds) > MS_WATERSHED: + seconds /= 1000 + dt = EPOCH + timedelta(seconds=seconds) + return dt.replace(tzinfo=timezone.utc) + + +def _parse_timezone(value: Optional[str]) -> Union[None, int, timezone]: + if value == "Z": + return timezone.utc + elif value is not None: + offset_mins = int(value[-2:]) if len(value) > 3 else 0 + offset = 60 * int(value[1:3]) + offset_mins + if value[0] == "-": + offset = -offset + return timezone(timedelta(minutes=offset)) + else: + return None + + +def parse_datetime(value: Union[datetime, StrBytesIntFloat]) -> datetime: + """ + Parse a datetime/int/float/string and return a datetime.datetime. + + This function supports time zone offsets. When the input contains one, + the output uses a timezone with a fixed offset from UTC. + + Raise ValueError if the input is well formatted but not a valid datetime. + Raise ValueError if the input isn't well formatted. + """ + if isinstance(value, datetime): + return value + + number = _get_numeric(value, "datetime") + if number is not None: + return _from_unix_seconds(number) + + if isinstance(value, bytes): + value = value.decode() + + assert not isinstance(value, (float, int)) + + match = datetime_re.match(value) + if match is None: + raise ValueError("invalid datetime format") + + kw = match.groupdict() + if kw["microsecond"]: + kw["microsecond"] = kw["microsecond"].ljust(6, "0") + + tzinfo = _parse_timezone(kw.pop("tzinfo")) + kw_: Dict[str, Union[None, int, timezone]] = {k: int(v) for k, v in kw.items() if v is not None} + kw_["tzinfo"] = tzinfo + + return datetime(**kw_) # type: ignore + + +def parse_date(value: Union[date, StrBytesIntFloat]) -> date: + """ + Parse a date/int/float/string and return a datetime.date. + + Raise ValueError if the input is well formatted but not a valid date. + Raise ValueError if the input isn't well formatted. + """ + if isinstance(value, date): + if isinstance(value, datetime): + return value.date() + else: + return value + + number = _get_numeric(value, "date") + if number is not None: + return _from_unix_seconds(number).date() + + if isinstance(value, bytes): + value = value.decode() + + assert not isinstance(value, (float, int)) + match = date_re.match(value) + if match is None: + raise ValueError("invalid date format") + + kw = {k: int(v) for k, v in match.groupdict().items()} + + try: + return date(**kw) + except ValueError: + raise ValueError("invalid date format") from None diff --git a/src/stagehand/_utils/_logs.py b/src/stagehand/_utils/_logs.py new file mode 100644 index 00000000..2d011968 --- /dev/null +++ b/src/stagehand/_utils/_logs.py @@ -0,0 +1,25 @@ +import os +import logging + +logger: logging.Logger = logging.getLogger("stagehand") +httpx_logger: logging.Logger = logging.getLogger("httpx") + + +def _basic_config() -> None: + # e.g. [2023-10-05 14:12:26 - stagehand._base_client:818 - DEBUG] HTTP Request: POST http://127.0.0.1:4010/foo/bar "200 OK" + logging.basicConfig( + format="[%(asctime)s - %(name)s:%(lineno)d - %(levelname)s] %(message)s", + datefmt="%Y-%m-%d %H:%M:%S", + ) + + +def setup_logging() -> None: + env = os.environ.get("STAGEHAND_LOG") + if env == "debug": + _basic_config() + logger.setLevel(logging.DEBUG) + httpx_logger.setLevel(logging.DEBUG) + elif env == "info": + _basic_config() + logger.setLevel(logging.INFO) + httpx_logger.setLevel(logging.INFO) diff --git a/src/stagehand/_utils/_proxy.py b/src/stagehand/_utils/_proxy.py new file mode 100644 index 00000000..0f239a33 --- /dev/null +++ b/src/stagehand/_utils/_proxy.py @@ -0,0 +1,65 @@ +from __future__ import annotations + +from abc import ABC, abstractmethod +from typing import Generic, TypeVar, Iterable, cast +from typing_extensions import override + +T = TypeVar("T") + + +class LazyProxy(Generic[T], ABC): + """Implements data methods to pretend that an instance is another instance. + + This includes forwarding attribute access and other methods. + """ + + # Note: we have to special case proxies that themselves return proxies + # to support using a proxy as a catch-all for any random access, e.g. `proxy.foo.bar.baz` + + def __getattr__(self, attr: str) -> object: + proxied = self.__get_proxied__() + if isinstance(proxied, LazyProxy): + return proxied # pyright: ignore + return getattr(proxied, attr) + + @override + def __repr__(self) -> str: + proxied = self.__get_proxied__() + if isinstance(proxied, LazyProxy): + return proxied.__class__.__name__ + return repr(self.__get_proxied__()) + + @override + def __str__(self) -> str: + proxied = self.__get_proxied__() + if isinstance(proxied, LazyProxy): + return proxied.__class__.__name__ + return str(proxied) + + @override + def __dir__(self) -> Iterable[str]: + proxied = self.__get_proxied__() + if isinstance(proxied, LazyProxy): + return [] + return proxied.__dir__() + + @property # type: ignore + @override + def __class__(self) -> type: # pyright: ignore + try: + proxied = self.__get_proxied__() + except Exception: + return type(self) + if issubclass(type(proxied), LazyProxy): + return type(proxied) + return proxied.__class__ + + def __get_proxied__(self) -> T: + return self.__load__() + + def __as_proxied__(self) -> T: + """Helper method that returns the current proxy, typed as the loaded object""" + return cast(T, self) + + @abstractmethod + def __load__(self) -> T: ... diff --git a/src/stagehand/_utils/_reflection.py b/src/stagehand/_utils/_reflection.py new file mode 100644 index 00000000..89aa712a --- /dev/null +++ b/src/stagehand/_utils/_reflection.py @@ -0,0 +1,42 @@ +from __future__ import annotations + +import inspect +from typing import Any, Callable + + +def function_has_argument(func: Callable[..., Any], arg_name: str) -> bool: + """Returns whether or not the given function has a specific parameter""" + sig = inspect.signature(func) + return arg_name in sig.parameters + + +def assert_signatures_in_sync( + source_func: Callable[..., Any], + check_func: Callable[..., Any], + *, + exclude_params: set[str] = set(), +) -> None: + """Ensure that the signature of the second function matches the first.""" + + check_sig = inspect.signature(check_func) + source_sig = inspect.signature(source_func) + + errors: list[str] = [] + + for name, source_param in source_sig.parameters.items(): + if name in exclude_params: + continue + + custom_param = check_sig.parameters.get(name) + if not custom_param: + errors.append(f"the `{name}` param is missing") + continue + + if custom_param.annotation != source_param.annotation: + errors.append( + f"types for the `{name}` param are do not match; source={repr(source_param.annotation)} checking={repr(custom_param.annotation)}" + ) + continue + + if errors: + raise AssertionError(f"{len(errors)} errors encountered when comparing signatures:\n\n" + "\n\n".join(errors)) diff --git a/src/stagehand/_utils/_resources_proxy.py b/src/stagehand/_utils/_resources_proxy.py new file mode 100644 index 00000000..92f333ed --- /dev/null +++ b/src/stagehand/_utils/_resources_proxy.py @@ -0,0 +1,24 @@ +from __future__ import annotations + +from typing import Any +from typing_extensions import override + +from ._proxy import LazyProxy + + +class ResourcesProxy(LazyProxy[Any]): + """A proxy for the `stagehand.resources` module. + + This is used so that we can lazily import `stagehand.resources` only when + needed *and* so that users can just import `stagehand` and reference `stagehand.resources` + """ + + @override + def __load__(self) -> Any: + import importlib + + mod = importlib.import_module("stagehand.resources") + return mod + + +resources = ResourcesProxy().__as_proxied__() diff --git a/src/stagehand/_utils/_streams.py b/src/stagehand/_utils/_streams.py new file mode 100644 index 00000000..f4a0208f --- /dev/null +++ b/src/stagehand/_utils/_streams.py @@ -0,0 +1,12 @@ +from typing import Any +from typing_extensions import Iterator, AsyncIterator + + +def consume_sync_iterator(iterator: Iterator[Any]) -> None: + for _ in iterator: + ... + + +async def consume_async_iterator(iterator: AsyncIterator[Any]) -> None: + async for _ in iterator: + ... diff --git a/src/stagehand/_utils/_sync.py b/src/stagehand/_utils/_sync.py new file mode 100644 index 00000000..f6027c18 --- /dev/null +++ b/src/stagehand/_utils/_sync.py @@ -0,0 +1,58 @@ +from __future__ import annotations + +import asyncio +import functools +from typing import TypeVar, Callable, Awaitable +from typing_extensions import ParamSpec + +import anyio +import sniffio +import anyio.to_thread + +T_Retval = TypeVar("T_Retval") +T_ParamSpec = ParamSpec("T_ParamSpec") + + +async def to_thread( + func: Callable[T_ParamSpec, T_Retval], /, *args: T_ParamSpec.args, **kwargs: T_ParamSpec.kwargs +) -> T_Retval: + if sniffio.current_async_library() == "asyncio": + return await asyncio.to_thread(func, *args, **kwargs) + + return await anyio.to_thread.run_sync( + functools.partial(func, *args, **kwargs), + ) + + +# inspired by `asyncer`, https://github.com/tiangolo/asyncer +def asyncify(function: Callable[T_ParamSpec, T_Retval]) -> Callable[T_ParamSpec, Awaitable[T_Retval]]: + """ + Take a blocking function and create an async one that receives the same + positional and keyword arguments. + + Usage: + + ```python + def blocking_func(arg1, arg2, kwarg1=None): + # blocking code + return result + + + result = asyncify(blocking_function)(arg1, arg2, kwarg1=value1) + ``` + + ## Arguments + + `function`: a blocking regular callable (e.g. a function) + + ## Return + + An async function that takes the same positional and keyword arguments as the + original one, that when called runs the same original function in a thread worker + and returns the result. + """ + + async def wrapper(*args: T_ParamSpec.args, **kwargs: T_ParamSpec.kwargs) -> T_Retval: + return await to_thread(function, *args, **kwargs) + + return wrapper diff --git a/src/stagehand/_utils/_transform.py b/src/stagehand/_utils/_transform.py new file mode 100644 index 00000000..52075492 --- /dev/null +++ b/src/stagehand/_utils/_transform.py @@ -0,0 +1,457 @@ +from __future__ import annotations + +import io +import base64 +import pathlib +from typing import Any, Mapping, TypeVar, cast +from datetime import date, datetime +from typing_extensions import Literal, get_args, override, get_type_hints as _get_type_hints + +import anyio +import pydantic + +from ._utils import ( + is_list, + is_given, + lru_cache, + is_mapping, + is_iterable, + is_sequence, +) +from .._files import is_base64_file_input +from ._compat import get_origin, is_typeddict +from ._typing import ( + is_list_type, + is_union_type, + extract_type_arg, + is_iterable_type, + is_required_type, + is_sequence_type, + is_annotated_type, + strip_annotated_type, +) + +_T = TypeVar("_T") + + +# TODO: support for drilling globals() and locals() +# TODO: ensure works correctly with forward references in all cases + + +PropertyFormat = Literal["iso8601", "base64", "custom"] + + +class PropertyInfo: + """Metadata class to be used in Annotated types to provide information about a given type. + + For example: + + class MyParams(TypedDict): + account_holder_name: Annotated[str, PropertyInfo(alias='accountHolderName')] + + This means that {'account_holder_name': 'Robert'} will be transformed to {'accountHolderName': 'Robert'} before being sent to the API. + """ + + alias: str | None + format: PropertyFormat | None + format_template: str | None + discriminator: str | None + + def __init__( + self, + *, + alias: str | None = None, + format: PropertyFormat | None = None, + format_template: str | None = None, + discriminator: str | None = None, + ) -> None: + self.alias = alias + self.format = format + self.format_template = format_template + self.discriminator = discriminator + + @override + def __repr__(self) -> str: + return f"{self.__class__.__name__}(alias='{self.alias}', format={self.format}, format_template='{self.format_template}', discriminator='{self.discriminator}')" + + +def maybe_transform( + data: object, + expected_type: object, +) -> Any | None: + """Wrapper over `transform()` that allows `None` to be passed. + + See `transform()` for more details. + """ + if data is None: + return None + return transform(data, expected_type) + + +# Wrapper over _transform_recursive providing fake types +def transform( + data: _T, + expected_type: object, +) -> _T: + """Transform dictionaries based off of type information from the given type, for example: + + ```py + class Params(TypedDict, total=False): + card_id: Required[Annotated[str, PropertyInfo(alias="cardID")]] + + + transformed = transform({"card_id": ""}, Params) + # {'cardID': ''} + ``` + + Any keys / data that does not have type information given will be included as is. + + It should be noted that the transformations that this function does are not represented in the type system. + """ + transformed = _transform_recursive(data, annotation=cast(type, expected_type)) + return cast(_T, transformed) + + +@lru_cache(maxsize=8096) +def _get_annotated_type(type_: type) -> type | None: + """If the given type is an `Annotated` type then it is returned, if not `None` is returned. + + This also unwraps the type when applicable, e.g. `Required[Annotated[T, ...]]` + """ + if is_required_type(type_): + # Unwrap `Required[Annotated[T, ...]]` to `Annotated[T, ...]` + type_ = get_args(type_)[0] + + if is_annotated_type(type_): + return type_ + + return None + + +def _maybe_transform_key(key: str, type_: type) -> str: + """Transform the given `data` based on the annotations provided in `type_`. + + Note: this function only looks at `Annotated` types that contain `PropertyInfo` metadata. + """ + annotated_type = _get_annotated_type(type_) + if annotated_type is None: + # no `Annotated` definition for this type, no transformation needed + return key + + # ignore the first argument as it is the actual type + annotations = get_args(annotated_type)[1:] + for annotation in annotations: + if isinstance(annotation, PropertyInfo) and annotation.alias is not None: + return annotation.alias + + return key + + +def _no_transform_needed(annotation: type) -> bool: + return annotation == float or annotation == int + + +def _transform_recursive( + data: object, + *, + annotation: type, + inner_type: type | None = None, +) -> object: + """Transform the given data against the expected type. + + Args: + annotation: The direct type annotation given to the particular piece of data. + This may or may not be wrapped in metadata types, e.g. `Required[T]`, `Annotated[T, ...]` etc + + inner_type: If applicable, this is the "inside" type. This is useful in certain cases where the outside type + is a container type such as `List[T]`. In that case `inner_type` should be set to `T` so that each entry in + the list can be transformed using the metadata from the container type. + + Defaults to the same value as the `annotation` argument. + """ + from .._compat import model_dump + + if inner_type is None: + inner_type = annotation + + stripped_type = strip_annotated_type(inner_type) + origin = get_origin(stripped_type) or stripped_type + if is_typeddict(stripped_type) and is_mapping(data): + return _transform_typeddict(data, stripped_type) + + if origin == dict and is_mapping(data): + items_type = get_args(stripped_type)[1] + return {key: _transform_recursive(value, annotation=items_type) for key, value in data.items()} + + if ( + # List[T] + (is_list_type(stripped_type) and is_list(data)) + # Iterable[T] + or (is_iterable_type(stripped_type) and is_iterable(data) and not isinstance(data, str)) + # Sequence[T] + or (is_sequence_type(stripped_type) and is_sequence(data) and not isinstance(data, str)) + ): + # dicts are technically iterable, but it is an iterable on the keys of the dict and is not usually + # intended as an iterable, so we don't transform it. + if isinstance(data, dict): + return cast(object, data) + + inner_type = extract_type_arg(stripped_type, 0) + if _no_transform_needed(inner_type): + # for some types there is no need to transform anything, so we can get a small + # perf boost from skipping that work. + # + # but we still need to convert to a list to ensure the data is json-serializable + if is_list(data): + return data + return list(data) + + return [_transform_recursive(d, annotation=annotation, inner_type=inner_type) for d in data] + + if is_union_type(stripped_type): + # For union types we run the transformation against all subtypes to ensure that everything is transformed. + # + # TODO: there may be edge cases where the same normalized field name will transform to two different names + # in different subtypes. + for subtype in get_args(stripped_type): + data = _transform_recursive(data, annotation=annotation, inner_type=subtype) + return data + + if isinstance(data, pydantic.BaseModel): + return model_dump(data, exclude_unset=True, mode="json") + + annotated_type = _get_annotated_type(annotation) + if annotated_type is None: + return data + + # ignore the first argument as it is the actual type + annotations = get_args(annotated_type)[1:] + for annotation in annotations: + if isinstance(annotation, PropertyInfo) and annotation.format is not None: + return _format_data(data, annotation.format, annotation.format_template) + + return data + + +def _format_data(data: object, format_: PropertyFormat, format_template: str | None) -> object: + if isinstance(data, (date, datetime)): + if format_ == "iso8601": + return data.isoformat() + + if format_ == "custom" and format_template is not None: + return data.strftime(format_template) + + if format_ == "base64" and is_base64_file_input(data): + binary: str | bytes | None = None + + if isinstance(data, pathlib.Path): + binary = data.read_bytes() + elif isinstance(data, io.IOBase): + binary = data.read() + + if isinstance(binary, str): # type: ignore[unreachable] + binary = binary.encode() + + if not isinstance(binary, bytes): + raise RuntimeError(f"Could not read bytes from {data}; Received {type(binary)}") + + return base64.b64encode(binary).decode("ascii") + + return data + + +def _transform_typeddict( + data: Mapping[str, object], + expected_type: type, +) -> Mapping[str, object]: + result: dict[str, object] = {} + annotations = get_type_hints(expected_type, include_extras=True) + for key, value in data.items(): + if not is_given(value): + # we don't need to include omitted values here as they'll + # be stripped out before the request is sent anyway + continue + + type_ = annotations.get(key) + if type_ is None: + # we do not have a type annotation for this field, leave it as is + result[key] = value + else: + result[_maybe_transform_key(key, type_)] = _transform_recursive(value, annotation=type_) + return result + + +async def async_maybe_transform( + data: object, + expected_type: object, +) -> Any | None: + """Wrapper over `async_transform()` that allows `None` to be passed. + + See `async_transform()` for more details. + """ + if data is None: + return None + return await async_transform(data, expected_type) + + +async def async_transform( + data: _T, + expected_type: object, +) -> _T: + """Transform dictionaries based off of type information from the given type, for example: + + ```py + class Params(TypedDict, total=False): + card_id: Required[Annotated[str, PropertyInfo(alias="cardID")]] + + + transformed = transform({"card_id": ""}, Params) + # {'cardID': ''} + ``` + + Any keys / data that does not have type information given will be included as is. + + It should be noted that the transformations that this function does are not represented in the type system. + """ + transformed = await _async_transform_recursive(data, annotation=cast(type, expected_type)) + return cast(_T, transformed) + + +async def _async_transform_recursive( + data: object, + *, + annotation: type, + inner_type: type | None = None, +) -> object: + """Transform the given data against the expected type. + + Args: + annotation: The direct type annotation given to the particular piece of data. + This may or may not be wrapped in metadata types, e.g. `Required[T]`, `Annotated[T, ...]` etc + + inner_type: If applicable, this is the "inside" type. This is useful in certain cases where the outside type + is a container type such as `List[T]`. In that case `inner_type` should be set to `T` so that each entry in + the list can be transformed using the metadata from the container type. + + Defaults to the same value as the `annotation` argument. + """ + from .._compat import model_dump + + if inner_type is None: + inner_type = annotation + + stripped_type = strip_annotated_type(inner_type) + origin = get_origin(stripped_type) or stripped_type + if is_typeddict(stripped_type) and is_mapping(data): + return await _async_transform_typeddict(data, stripped_type) + + if origin == dict and is_mapping(data): + items_type = get_args(stripped_type)[1] + return {key: _transform_recursive(value, annotation=items_type) for key, value in data.items()} + + if ( + # List[T] + (is_list_type(stripped_type) and is_list(data)) + # Iterable[T] + or (is_iterable_type(stripped_type) and is_iterable(data) and not isinstance(data, str)) + # Sequence[T] + or (is_sequence_type(stripped_type) and is_sequence(data) and not isinstance(data, str)) + ): + # dicts are technically iterable, but it is an iterable on the keys of the dict and is not usually + # intended as an iterable, so we don't transform it. + if isinstance(data, dict): + return cast(object, data) + + inner_type = extract_type_arg(stripped_type, 0) + if _no_transform_needed(inner_type): + # for some types there is no need to transform anything, so we can get a small + # perf boost from skipping that work. + # + # but we still need to convert to a list to ensure the data is json-serializable + if is_list(data): + return data + return list(data) + + return [await _async_transform_recursive(d, annotation=annotation, inner_type=inner_type) for d in data] + + if is_union_type(stripped_type): + # For union types we run the transformation against all subtypes to ensure that everything is transformed. + # + # TODO: there may be edge cases where the same normalized field name will transform to two different names + # in different subtypes. + for subtype in get_args(stripped_type): + data = await _async_transform_recursive(data, annotation=annotation, inner_type=subtype) + return data + + if isinstance(data, pydantic.BaseModel): + return model_dump(data, exclude_unset=True, mode="json") + + annotated_type = _get_annotated_type(annotation) + if annotated_type is None: + return data + + # ignore the first argument as it is the actual type + annotations = get_args(annotated_type)[1:] + for annotation in annotations: + if isinstance(annotation, PropertyInfo) and annotation.format is not None: + return await _async_format_data(data, annotation.format, annotation.format_template) + + return data + + +async def _async_format_data(data: object, format_: PropertyFormat, format_template: str | None) -> object: + if isinstance(data, (date, datetime)): + if format_ == "iso8601": + return data.isoformat() + + if format_ == "custom" and format_template is not None: + return data.strftime(format_template) + + if format_ == "base64" and is_base64_file_input(data): + binary: str | bytes | None = None + + if isinstance(data, pathlib.Path): + binary = await anyio.Path(data).read_bytes() + elif isinstance(data, io.IOBase): + binary = data.read() + + if isinstance(binary, str): # type: ignore[unreachable] + binary = binary.encode() + + if not isinstance(binary, bytes): + raise RuntimeError(f"Could not read bytes from {data}; Received {type(binary)}") + + return base64.b64encode(binary).decode("ascii") + + return data + + +async def _async_transform_typeddict( + data: Mapping[str, object], + expected_type: type, +) -> Mapping[str, object]: + result: dict[str, object] = {} + annotations = get_type_hints(expected_type, include_extras=True) + for key, value in data.items(): + if not is_given(value): + # we don't need to include omitted values here as they'll + # be stripped out before the request is sent anyway + continue + + type_ = annotations.get(key) + if type_ is None: + # we do not have a type annotation for this field, leave it as is + result[key] = value + else: + result[_maybe_transform_key(key, type_)] = await _async_transform_recursive(value, annotation=type_) + return result + + +@lru_cache(maxsize=8096) +def get_type_hints( + obj: Any, + globalns: dict[str, Any] | None = None, + localns: Mapping[str, Any] | None = None, + include_extras: bool = False, +) -> dict[str, Any]: + return _get_type_hints(obj, globalns=globalns, localns=localns, include_extras=include_extras) diff --git a/src/stagehand/_utils/_typing.py b/src/stagehand/_utils/_typing.py new file mode 100644 index 00000000..193109f3 --- /dev/null +++ b/src/stagehand/_utils/_typing.py @@ -0,0 +1,156 @@ +from __future__ import annotations + +import sys +import typing +import typing_extensions +from typing import Any, TypeVar, Iterable, cast +from collections import abc as _c_abc +from typing_extensions import ( + TypeIs, + Required, + Annotated, + get_args, + get_origin, +) + +from ._utils import lru_cache +from .._types import InheritsGeneric +from ._compat import is_union as _is_union + + +def is_annotated_type(typ: type) -> bool: + return get_origin(typ) == Annotated + + +def is_list_type(typ: type) -> bool: + return (get_origin(typ) or typ) == list + + +def is_sequence_type(typ: type) -> bool: + origin = get_origin(typ) or typ + return origin == typing_extensions.Sequence or origin == typing.Sequence or origin == _c_abc.Sequence + + +def is_iterable_type(typ: type) -> bool: + """If the given type is `typing.Iterable[T]`""" + origin = get_origin(typ) or typ + return origin == Iterable or origin == _c_abc.Iterable + + +def is_union_type(typ: type) -> bool: + return _is_union(get_origin(typ)) + + +def is_required_type(typ: type) -> bool: + return get_origin(typ) == Required + + +def is_typevar(typ: type) -> bool: + # type ignore is required because type checkers + # think this expression will always return False + return type(typ) == TypeVar # type: ignore + + +_TYPE_ALIAS_TYPES: tuple[type[typing_extensions.TypeAliasType], ...] = (typing_extensions.TypeAliasType,) +if sys.version_info >= (3, 12): + _TYPE_ALIAS_TYPES = (*_TYPE_ALIAS_TYPES, typing.TypeAliasType) + + +def is_type_alias_type(tp: Any, /) -> TypeIs[typing_extensions.TypeAliasType]: + """Return whether the provided argument is an instance of `TypeAliasType`. + + ```python + type Int = int + is_type_alias_type(Int) + # > True + Str = TypeAliasType("Str", str) + is_type_alias_type(Str) + # > True + ``` + """ + return isinstance(tp, _TYPE_ALIAS_TYPES) + + +# Extracts T from Annotated[T, ...] or from Required[Annotated[T, ...]] +@lru_cache(maxsize=8096) +def strip_annotated_type(typ: type) -> type: + if is_required_type(typ) or is_annotated_type(typ): + return strip_annotated_type(cast(type, get_args(typ)[0])) + + return typ + + +def extract_type_arg(typ: type, index: int) -> type: + args = get_args(typ) + try: + return cast(type, args[index]) + except IndexError as err: + raise RuntimeError(f"Expected type {typ} to have a type argument at index {index} but it did not") from err + + +def extract_type_var_from_base( + typ: type, + *, + generic_bases: tuple[type, ...], + index: int, + failure_message: str | None = None, +) -> type: + """Given a type like `Foo[T]`, returns the generic type variable `T`. + + This also handles the case where a concrete subclass is given, e.g. + ```py + class MyResponse(Foo[bytes]): + ... + + extract_type_var(MyResponse, bases=(Foo,), index=0) -> bytes + ``` + + And where a generic subclass is given: + ```py + _T = TypeVar('_T') + class MyResponse(Foo[_T]): + ... + + extract_type_var(MyResponse[bytes], bases=(Foo,), index=0) -> bytes + ``` + """ + cls = cast(object, get_origin(typ) or typ) + if cls in generic_bases: # pyright: ignore[reportUnnecessaryContains] + # we're given the class directly + return extract_type_arg(typ, index) + + # if a subclass is given + # --- + # this is needed as __orig_bases__ is not present in the typeshed stubs + # because it is intended to be for internal use only, however there does + # not seem to be a way to resolve generic TypeVars for inherited subclasses + # without using it. + if isinstance(cls, InheritsGeneric): + target_base_class: Any | None = None + for base in cls.__orig_bases__: + if base.__origin__ in generic_bases: + target_base_class = base + break + + if target_base_class is None: + raise RuntimeError( + "Could not find the generic base class;\n" + "This should never happen;\n" + f"Does {cls} inherit from one of {generic_bases} ?" + ) + + extracted = extract_type_arg(target_base_class, index) + if is_typevar(extracted): + # If the extracted type argument is itself a type variable + # then that means the subclass itself is generic, so we have + # to resolve the type argument from the class itself, not + # the base class. + # + # Note: if there is more than 1 type argument, the subclass could + # change the ordering of the type arguments, this is not currently + # supported. + return extract_type_arg(typ, index) + + return extracted + + raise RuntimeError(failure_message or f"Could not resolve inner type variable at index {index} for {typ}") diff --git a/src/stagehand/_utils/_utils.py b/src/stagehand/_utils/_utils.py new file mode 100644 index 00000000..eec7f4a1 --- /dev/null +++ b/src/stagehand/_utils/_utils.py @@ -0,0 +1,421 @@ +from __future__ import annotations + +import os +import re +import inspect +import functools +from typing import ( + Any, + Tuple, + Mapping, + TypeVar, + Callable, + Iterable, + Sequence, + cast, + overload, +) +from pathlib import Path +from datetime import date, datetime +from typing_extensions import TypeGuard + +import sniffio + +from .._types import Omit, NotGiven, FileTypes, HeadersLike + +_T = TypeVar("_T") +_TupleT = TypeVar("_TupleT", bound=Tuple[object, ...]) +_MappingT = TypeVar("_MappingT", bound=Mapping[str, object]) +_SequenceT = TypeVar("_SequenceT", bound=Sequence[object]) +CallableT = TypeVar("CallableT", bound=Callable[..., Any]) + + +def flatten(t: Iterable[Iterable[_T]]) -> list[_T]: + return [item for sublist in t for item in sublist] + + +def extract_files( + # TODO: this needs to take Dict but variance issues..... + # create protocol type ? + query: Mapping[str, object], + *, + paths: Sequence[Sequence[str]], +) -> list[tuple[str, FileTypes]]: + """Recursively extract files from the given dictionary based on specified paths. + + A path may look like this ['foo', 'files', '', 'data']. + + Note: this mutates the given dictionary. + """ + files: list[tuple[str, FileTypes]] = [] + for path in paths: + files.extend(_extract_items(query, path, index=0, flattened_key=None)) + return files + + +def _extract_items( + obj: object, + path: Sequence[str], + *, + index: int, + flattened_key: str | None, +) -> list[tuple[str, FileTypes]]: + try: + key = path[index] + except IndexError: + if not is_given(obj): + # no value was provided - we can safely ignore + return [] + + # cyclical import + from .._files import assert_is_file_content + + # We have exhausted the path, return the entry we found. + assert flattened_key is not None + + if is_list(obj): + files: list[tuple[str, FileTypes]] = [] + for entry in obj: + assert_is_file_content(entry, key=flattened_key + "[]" if flattened_key else "") + files.append((flattened_key + "[]", cast(FileTypes, entry))) + return files + + assert_is_file_content(obj, key=flattened_key) + return [(flattened_key, cast(FileTypes, obj))] + + index += 1 + if is_dict(obj): + try: + # We are at the last entry in the path so we must remove the field + if (len(path)) == index: + item = obj.pop(key) + else: + item = obj[key] + except KeyError: + # Key was not present in the dictionary, this is not indicative of an error + # as the given path may not point to a required field. We also do not want + # to enforce required fields as the API may differ from the spec in some cases. + return [] + if flattened_key is None: + flattened_key = key + else: + flattened_key += f"[{key}]" + return _extract_items( + item, + path, + index=index, + flattened_key=flattened_key, + ) + elif is_list(obj): + if key != "": + return [] + + return flatten( + [ + _extract_items( + item, + path, + index=index, + flattened_key=flattened_key + "[]" if flattened_key is not None else "[]", + ) + for item in obj + ] + ) + + # Something unexpected was passed, just ignore it. + return [] + + +def is_given(obj: _T | NotGiven | Omit) -> TypeGuard[_T]: + return not isinstance(obj, NotGiven) and not isinstance(obj, Omit) + + +# Type safe methods for narrowing types with TypeVars. +# The default narrowing for isinstance(obj, dict) is dict[unknown, unknown], +# however this cause Pyright to rightfully report errors. As we know we don't +# care about the contained types we can safely use `object` in its place. +# +# There are two separate functions defined, `is_*` and `is_*_t` for different use cases. +# `is_*` is for when you're dealing with an unknown input +# `is_*_t` is for when you're narrowing a known union type to a specific subset + + +def is_tuple(obj: object) -> TypeGuard[tuple[object, ...]]: + return isinstance(obj, tuple) + + +def is_tuple_t(obj: _TupleT | object) -> TypeGuard[_TupleT]: + return isinstance(obj, tuple) + + +def is_sequence(obj: object) -> TypeGuard[Sequence[object]]: + return isinstance(obj, Sequence) + + +def is_sequence_t(obj: _SequenceT | object) -> TypeGuard[_SequenceT]: + return isinstance(obj, Sequence) + + +def is_mapping(obj: object) -> TypeGuard[Mapping[str, object]]: + return isinstance(obj, Mapping) + + +def is_mapping_t(obj: _MappingT | object) -> TypeGuard[_MappingT]: + return isinstance(obj, Mapping) + + +def is_dict(obj: object) -> TypeGuard[dict[object, object]]: + return isinstance(obj, dict) + + +def is_list(obj: object) -> TypeGuard[list[object]]: + return isinstance(obj, list) + + +def is_iterable(obj: object) -> TypeGuard[Iterable[object]]: + return isinstance(obj, Iterable) + + +def deepcopy_minimal(item: _T) -> _T: + """Minimal reimplementation of copy.deepcopy() that will only copy certain object types: + + - mappings, e.g. `dict` + - list + + This is done for performance reasons. + """ + if is_mapping(item): + return cast(_T, {k: deepcopy_minimal(v) for k, v in item.items()}) + if is_list(item): + return cast(_T, [deepcopy_minimal(entry) for entry in item]) + return item + + +# copied from https://github.com/Rapptz/RoboDanny +def human_join(seq: Sequence[str], *, delim: str = ", ", final: str = "or") -> str: + size = len(seq) + if size == 0: + return "" + + if size == 1: + return seq[0] + + if size == 2: + return f"{seq[0]} {final} {seq[1]}" + + return delim.join(seq[:-1]) + f" {final} {seq[-1]}" + + +def quote(string: str) -> str: + """Add single quotation marks around the given string. Does *not* do any escaping.""" + return f"'{string}'" + + +def required_args(*variants: Sequence[str]) -> Callable[[CallableT], CallableT]: + """Decorator to enforce a given set of arguments or variants of arguments are passed to the decorated function. + + Useful for enforcing runtime validation of overloaded functions. + + Example usage: + ```py + @overload + def foo(*, a: str) -> str: ... + + + @overload + def foo(*, b: bool) -> str: ... + + + # This enforces the same constraints that a static type checker would + # i.e. that either a or b must be passed to the function + @required_args(["a"], ["b"]) + def foo(*, a: str | None = None, b: bool | None = None) -> str: ... + ``` + """ + + def inner(func: CallableT) -> CallableT: + params = inspect.signature(func).parameters + positional = [ + name + for name, param in params.items() + if param.kind + in { + param.POSITIONAL_ONLY, + param.POSITIONAL_OR_KEYWORD, + } + ] + + @functools.wraps(func) + def wrapper(*args: object, **kwargs: object) -> object: + given_params: set[str] = set() + for i, _ in enumerate(args): + try: + given_params.add(positional[i]) + except IndexError: + raise TypeError( + f"{func.__name__}() takes {len(positional)} argument(s) but {len(args)} were given" + ) from None + + for key in kwargs.keys(): + given_params.add(key) + + for variant in variants: + matches = all((param in given_params for param in variant)) + if matches: + break + else: # no break + if len(variants) > 1: + variations = human_join( + ["(" + human_join([quote(arg) for arg in variant], final="and") + ")" for variant in variants] + ) + msg = f"Missing required arguments; Expected either {variations} arguments to be given" + else: + assert len(variants) > 0 + + # TODO: this error message is not deterministic + missing = list(set(variants[0]) - given_params) + if len(missing) > 1: + msg = f"Missing required arguments: {human_join([quote(arg) for arg in missing])}" + else: + msg = f"Missing required argument: {quote(missing[0])}" + raise TypeError(msg) + return func(*args, **kwargs) + + return wrapper # type: ignore + + return inner + + +_K = TypeVar("_K") +_V = TypeVar("_V") + + +@overload +def strip_not_given(obj: None) -> None: ... + + +@overload +def strip_not_given(obj: Mapping[_K, _V | NotGiven]) -> dict[_K, _V]: ... + + +@overload +def strip_not_given(obj: object) -> object: ... + + +def strip_not_given(obj: object | None) -> object: + """Remove all top-level keys where their values are instances of `NotGiven`""" + if obj is None: + return None + + if not is_mapping(obj): + return obj + + return {key: value for key, value in obj.items() if not isinstance(value, NotGiven)} + + +def coerce_integer(val: str) -> int: + return int(val, base=10) + + +def coerce_float(val: str) -> float: + return float(val) + + +def coerce_boolean(val: str) -> bool: + return val == "true" or val == "1" or val == "on" + + +def maybe_coerce_integer(val: str | None) -> int | None: + if val is None: + return None + return coerce_integer(val) + + +def maybe_coerce_float(val: str | None) -> float | None: + if val is None: + return None + return coerce_float(val) + + +def maybe_coerce_boolean(val: str | None) -> bool | None: + if val is None: + return None + return coerce_boolean(val) + + +def removeprefix(string: str, prefix: str) -> str: + """Remove a prefix from a string. + + Backport of `str.removeprefix` for Python < 3.9 + """ + if string.startswith(prefix): + return string[len(prefix) :] + return string + + +def removesuffix(string: str, suffix: str) -> str: + """Remove a suffix from a string. + + Backport of `str.removesuffix` for Python < 3.9 + """ + if string.endswith(suffix): + return string[: -len(suffix)] + return string + + +def file_from_path(path: str) -> FileTypes: + contents = Path(path).read_bytes() + file_name = os.path.basename(path) + return (file_name, contents) + + +def get_required_header(headers: HeadersLike, header: str) -> str: + lower_header = header.lower() + if is_mapping_t(headers): + # mypy doesn't understand the type narrowing here + for k, v in headers.items(): # type: ignore + if k.lower() == lower_header and isinstance(v, str): + return v + + # to deal with the case where the header looks like Stainless-Event-Id + intercaps_header = re.sub(r"([^\w])(\w)", lambda pat: pat.group(1) + pat.group(2).upper(), header.capitalize()) + + for normalized_header in [header, lower_header, header.upper(), intercaps_header]: + value = headers.get(normalized_header) + if value: + return value + + raise ValueError(f"Could not find {header} header") + + +def get_async_library() -> str: + try: + return sniffio.current_async_library() + except Exception: + return "false" + + +def lru_cache(*, maxsize: int | None = 128) -> Callable[[CallableT], CallableT]: + """A version of functools.lru_cache that retains the type signature + for the wrapped function arguments. + """ + wrapper = functools.lru_cache( # noqa: TID251 + maxsize=maxsize, + ) + return cast(Any, wrapper) # type: ignore[no-any-return] + + +def json_safe(data: object) -> object: + """Translates a mapping / sequence recursively in the same fashion + as `pydantic` v2's `model_dump(mode="json")`. + """ + if is_mapping(data): + return {json_safe(key): json_safe(value) for key, value in data.items()} + + if is_iterable(data) and not isinstance(data, (str, bytes, bytearray)): + return [json_safe(item) for item in data] + + if isinstance(data, (datetime, date)): + return data.isoformat() + + return data diff --git a/src/stagehand/_version.py b/src/stagehand/_version.py new file mode 100644 index 00000000..af4c4034 --- /dev/null +++ b/src/stagehand/_version.py @@ -0,0 +1,4 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +__title__ = "stagehand" +__version__ = "0.0.1" diff --git a/src/stagehand/lib/.keep b/src/stagehand/lib/.keep new file mode 100644 index 00000000..5e2c99fd --- /dev/null +++ b/src/stagehand/lib/.keep @@ -0,0 +1,4 @@ +File generated from our OpenAPI spec by Stainless. + +This directory can be used to store custom files to expand the SDK. +It is ignored by Stainless code generation and its content (other than this keep file) won't be touched. \ No newline at end of file diff --git a/src/stagehand/py.typed b/src/stagehand/py.typed new file mode 100644 index 00000000..e69de29b diff --git a/src/stagehand/resources/__init__.py b/src/stagehand/resources/__init__.py new file mode 100644 index 00000000..cc3467b6 --- /dev/null +++ b/src/stagehand/resources/__init__.py @@ -0,0 +1,19 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from .sessions import ( + SessionsResource, + AsyncSessionsResource, + SessionsResourceWithRawResponse, + AsyncSessionsResourceWithRawResponse, + SessionsResourceWithStreamingResponse, + AsyncSessionsResourceWithStreamingResponse, +) + +__all__ = [ + "SessionsResource", + "AsyncSessionsResource", + "SessionsResourceWithRawResponse", + "AsyncSessionsResourceWithRawResponse", + "SessionsResourceWithStreamingResponse", + "AsyncSessionsResourceWithStreamingResponse", +] diff --git a/src/stagehand/resources/sessions.py b/src/stagehand/resources/sessions.py new file mode 100644 index 00000000..0135a438 --- /dev/null +++ b/src/stagehand/resources/sessions.py @@ -0,0 +1,963 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Any, Dict, Optional, cast +from typing_extensions import Literal + +import httpx + +from ..types import ( + session_act_params, + session_start_params, + session_extract_params, + session_observe_params, + session_navigate_params, + session_execute_agent_params, +) +from .._types import Body, Omit, Query, Headers, NotGiven, omit, not_given +from .._utils import is_given, maybe_transform, strip_not_given, async_maybe_transform +from .._compat import cached_property +from .._resource import SyncAPIResource, AsyncAPIResource +from .._response import ( + to_raw_response_wrapper, + to_streamed_response_wrapper, + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, +) +from .._base_client import make_request_options +from ..types.session_act_response import SessionActResponse +from ..types.session_end_response import SessionEndResponse +from ..types.session_start_response import SessionStartResponse +from ..types.session_extract_response import SessionExtractResponse +from ..types.session_observe_response import SessionObserveResponse +from ..types.session_navigate_response import SessionNavigateResponse +from ..types.session_execute_agent_response import SessionExecuteAgentResponse + +__all__ = ["SessionsResource", "AsyncSessionsResource"] + + +class SessionsResource(SyncAPIResource): + @cached_property + def with_raw_response(self) -> SessionsResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/stainless-sdks/stagehand-python#accessing-raw-response-data-eg-headers + """ + return SessionsResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> SessionsResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/stainless-sdks/stagehand-python#with_streaming_response + """ + return SessionsResourceWithStreamingResponse(self) + + def act( + self, + session_id: str, + *, + input: session_act_params.Input, + frame_id: str | Omit = omit, + options: session_act_params.Options | Omit = omit, + x_stream_response: Literal["true", "false"] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> SessionActResponse: + """ + Performs a browser action based on natural language instruction or a specific + action object returned by observe(). + + Args: + input: Natural language instruction + + frame_id: Frame ID to act on (optional) + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not session_id: + raise ValueError(f"Expected a non-empty value for `session_id` but received {session_id!r}") + extra_headers = { + **strip_not_given( + {"x-stream-response": str(x_stream_response) if is_given(x_stream_response) else not_given} + ), + **(extra_headers or {}), + } + return self._post( + f"/sessions/{session_id}/act", + body=maybe_transform( + { + "input": input, + "frame_id": frame_id, + "options": options, + }, + session_act_params.SessionActParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=SessionActResponse, + ) + + def end( + self, + session_id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> SessionEndResponse: + """ + Closes the browser and cleans up all resources associated with the session. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not session_id: + raise ValueError(f"Expected a non-empty value for `session_id` but received {session_id!r}") + return self._post( + f"/sessions/{session_id}/end", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=SessionEndResponse, + ) + + def execute_agent( + self, + session_id: str, + *, + agent_config: session_execute_agent_params.AgentConfig, + execute_options: session_execute_agent_params.ExecuteOptions, + frame_id: str | Omit = omit, + x_stream_response: Literal["true", "false"] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> SessionExecuteAgentResponse: + """ + Runs an autonomous agent that can perform multiple actions to complete a complex + task. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not session_id: + raise ValueError(f"Expected a non-empty value for `session_id` but received {session_id!r}") + extra_headers = { + **strip_not_given( + {"x-stream-response": str(x_stream_response) if is_given(x_stream_response) else not_given} + ), + **(extra_headers or {}), + } + return self._post( + f"/sessions/{session_id}/agentExecute", + body=maybe_transform( + { + "agent_config": agent_config, + "execute_options": execute_options, + "frame_id": frame_id, + }, + session_execute_agent_params.SessionExecuteAgentParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=SessionExecuteAgentResponse, + ) + + def extract( + self, + session_id: str, + *, + frame_id: str | Omit = omit, + instruction: str | Omit = omit, + options: session_extract_params.Options | Omit = omit, + schema: Dict[str, object] | Omit = omit, + x_stream_response: Literal["true", "false"] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> SessionExtractResponse: + """ + Extracts data from the current page using natural language instructions and + optional JSON schema for structured output. + + Args: + frame_id: Frame ID to extract from + + instruction: Natural language instruction for extraction + + schema: JSON Schema for structured output + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not session_id: + raise ValueError(f"Expected a non-empty value for `session_id` but received {session_id!r}") + extra_headers = { + **strip_not_given( + {"x-stream-response": str(x_stream_response) if is_given(x_stream_response) else not_given} + ), + **(extra_headers or {}), + } + return cast( + SessionExtractResponse, + self._post( + f"/sessions/{session_id}/extract", + body=maybe_transform( + { + "frame_id": frame_id, + "instruction": instruction, + "options": options, + "schema": schema, + }, + session_extract_params.SessionExtractParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=cast( + Any, SessionExtractResponse + ), # Union types cannot be passed in as arguments in the type system + ), + ) + + def navigate( + self, + session_id: str, + *, + url: str, + frame_id: str | Omit = omit, + options: session_navigate_params.Options | Omit = omit, + x_stream_response: Literal["true", "false"] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> Optional[SessionNavigateResponse]: + """ + Navigates the browser to the specified URL and waits for page load. + + Args: + url: URL to navigate to + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not session_id: + raise ValueError(f"Expected a non-empty value for `session_id` but received {session_id!r}") + extra_headers = { + **strip_not_given( + {"x-stream-response": str(x_stream_response) if is_given(x_stream_response) else not_given} + ), + **(extra_headers or {}), + } + return self._post( + f"/sessions/{session_id}/navigate", + body=maybe_transform( + { + "url": url, + "frame_id": frame_id, + "options": options, + }, + session_navigate_params.SessionNavigateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=SessionNavigateResponse, + ) + + def observe( + self, + session_id: str, + *, + frame_id: str | Omit = omit, + instruction: str | Omit = omit, + options: session_observe_params.Options | Omit = omit, + x_stream_response: Literal["true", "false"] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> SessionObserveResponse: + """ + Returns a list of candidate actions that can be performed on the page, + optionally filtered by natural language instruction. + + Args: + frame_id: Frame ID to observe + + instruction: Natural language instruction to filter actions + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not session_id: + raise ValueError(f"Expected a non-empty value for `session_id` but received {session_id!r}") + extra_headers = { + **strip_not_given( + {"x-stream-response": str(x_stream_response) if is_given(x_stream_response) else not_given} + ), + **(extra_headers or {}), + } + return self._post( + f"/sessions/{session_id}/observe", + body=maybe_transform( + { + "frame_id": frame_id, + "instruction": instruction, + "options": options, + }, + session_observe_params.SessionObserveParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=SessionObserveResponse, + ) + + def start( + self, + *, + env: Literal["LOCAL", "BROWSERBASE"], + api_key: str | Omit = omit, + dom_settle_timeout: int | Omit = omit, + local_browser_launch_options: session_start_params.LocalBrowserLaunchOptions | Omit = omit, + model: str | Omit = omit, + project_id: str | Omit = omit, + self_heal: bool | Omit = omit, + system_prompt: str | Omit = omit, + verbose: int | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> SessionStartResponse: + """Initializes a new Stagehand session with a browser instance. + + Returns a session + ID that must be used for all subsequent requests. + + Args: + env: Environment to run the browser in + + api_key: API key for Browserbase (required when env=BROWSERBASE) + + dom_settle_timeout: Timeout in ms to wait for DOM to settle + + local_browser_launch_options: Options for local browser launch + + model: AI model to use for actions + + project_id: Project ID for Browserbase (required when env=BROWSERBASE) + + self_heal: Enable self-healing for failed actions + + system_prompt: Custom system prompt for AI actions + + verbose: Logging verbosity level + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return self._post( + "/sessions/start", + body=maybe_transform( + { + "env": env, + "api_key": api_key, + "dom_settle_timeout": dom_settle_timeout, + "local_browser_launch_options": local_browser_launch_options, + "model": model, + "project_id": project_id, + "self_heal": self_heal, + "system_prompt": system_prompt, + "verbose": verbose, + }, + session_start_params.SessionStartParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=SessionStartResponse, + ) + + +class AsyncSessionsResource(AsyncAPIResource): + @cached_property + def with_raw_response(self) -> AsyncSessionsResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/stainless-sdks/stagehand-python#accessing-raw-response-data-eg-headers + """ + return AsyncSessionsResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncSessionsResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/stainless-sdks/stagehand-python#with_streaming_response + """ + return AsyncSessionsResourceWithStreamingResponse(self) + + async def act( + self, + session_id: str, + *, + input: session_act_params.Input, + frame_id: str | Omit = omit, + options: session_act_params.Options | Omit = omit, + x_stream_response: Literal["true", "false"] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> SessionActResponse: + """ + Performs a browser action based on natural language instruction or a specific + action object returned by observe(). + + Args: + input: Natural language instruction + + frame_id: Frame ID to act on (optional) + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not session_id: + raise ValueError(f"Expected a non-empty value for `session_id` but received {session_id!r}") + extra_headers = { + **strip_not_given( + {"x-stream-response": str(x_stream_response) if is_given(x_stream_response) else not_given} + ), + **(extra_headers or {}), + } + return await self._post( + f"/sessions/{session_id}/act", + body=await async_maybe_transform( + { + "input": input, + "frame_id": frame_id, + "options": options, + }, + session_act_params.SessionActParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=SessionActResponse, + ) + + async def end( + self, + session_id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> SessionEndResponse: + """ + Closes the browser and cleans up all resources associated with the session. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not session_id: + raise ValueError(f"Expected a non-empty value for `session_id` but received {session_id!r}") + return await self._post( + f"/sessions/{session_id}/end", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=SessionEndResponse, + ) + + async def execute_agent( + self, + session_id: str, + *, + agent_config: session_execute_agent_params.AgentConfig, + execute_options: session_execute_agent_params.ExecuteOptions, + frame_id: str | Omit = omit, + x_stream_response: Literal["true", "false"] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> SessionExecuteAgentResponse: + """ + Runs an autonomous agent that can perform multiple actions to complete a complex + task. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not session_id: + raise ValueError(f"Expected a non-empty value for `session_id` but received {session_id!r}") + extra_headers = { + **strip_not_given( + {"x-stream-response": str(x_stream_response) if is_given(x_stream_response) else not_given} + ), + **(extra_headers or {}), + } + return await self._post( + f"/sessions/{session_id}/agentExecute", + body=await async_maybe_transform( + { + "agent_config": agent_config, + "execute_options": execute_options, + "frame_id": frame_id, + }, + session_execute_agent_params.SessionExecuteAgentParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=SessionExecuteAgentResponse, + ) + + async def extract( + self, + session_id: str, + *, + frame_id: str | Omit = omit, + instruction: str | Omit = omit, + options: session_extract_params.Options | Omit = omit, + schema: Dict[str, object] | Omit = omit, + x_stream_response: Literal["true", "false"] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> SessionExtractResponse: + """ + Extracts data from the current page using natural language instructions and + optional JSON schema for structured output. + + Args: + frame_id: Frame ID to extract from + + instruction: Natural language instruction for extraction + + schema: JSON Schema for structured output + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not session_id: + raise ValueError(f"Expected a non-empty value for `session_id` but received {session_id!r}") + extra_headers = { + **strip_not_given( + {"x-stream-response": str(x_stream_response) if is_given(x_stream_response) else not_given} + ), + **(extra_headers or {}), + } + return cast( + SessionExtractResponse, + await self._post( + f"/sessions/{session_id}/extract", + body=await async_maybe_transform( + { + "frame_id": frame_id, + "instruction": instruction, + "options": options, + "schema": schema, + }, + session_extract_params.SessionExtractParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=cast( + Any, SessionExtractResponse + ), # Union types cannot be passed in as arguments in the type system + ), + ) + + async def navigate( + self, + session_id: str, + *, + url: str, + frame_id: str | Omit = omit, + options: session_navigate_params.Options | Omit = omit, + x_stream_response: Literal["true", "false"] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> Optional[SessionNavigateResponse]: + """ + Navigates the browser to the specified URL and waits for page load. + + Args: + url: URL to navigate to + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not session_id: + raise ValueError(f"Expected a non-empty value for `session_id` but received {session_id!r}") + extra_headers = { + **strip_not_given( + {"x-stream-response": str(x_stream_response) if is_given(x_stream_response) else not_given} + ), + **(extra_headers or {}), + } + return await self._post( + f"/sessions/{session_id}/navigate", + body=await async_maybe_transform( + { + "url": url, + "frame_id": frame_id, + "options": options, + }, + session_navigate_params.SessionNavigateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=SessionNavigateResponse, + ) + + async def observe( + self, + session_id: str, + *, + frame_id: str | Omit = omit, + instruction: str | Omit = omit, + options: session_observe_params.Options | Omit = omit, + x_stream_response: Literal["true", "false"] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> SessionObserveResponse: + """ + Returns a list of candidate actions that can be performed on the page, + optionally filtered by natural language instruction. + + Args: + frame_id: Frame ID to observe + + instruction: Natural language instruction to filter actions + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not session_id: + raise ValueError(f"Expected a non-empty value for `session_id` but received {session_id!r}") + extra_headers = { + **strip_not_given( + {"x-stream-response": str(x_stream_response) if is_given(x_stream_response) else not_given} + ), + **(extra_headers or {}), + } + return await self._post( + f"/sessions/{session_id}/observe", + body=await async_maybe_transform( + { + "frame_id": frame_id, + "instruction": instruction, + "options": options, + }, + session_observe_params.SessionObserveParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=SessionObserveResponse, + ) + + async def start( + self, + *, + env: Literal["LOCAL", "BROWSERBASE"], + api_key: str | Omit = omit, + dom_settle_timeout: int | Omit = omit, + local_browser_launch_options: session_start_params.LocalBrowserLaunchOptions | Omit = omit, + model: str | Omit = omit, + project_id: str | Omit = omit, + self_heal: bool | Omit = omit, + system_prompt: str | Omit = omit, + verbose: int | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> SessionStartResponse: + """Initializes a new Stagehand session with a browser instance. + + Returns a session + ID that must be used for all subsequent requests. + + Args: + env: Environment to run the browser in + + api_key: API key for Browserbase (required when env=BROWSERBASE) + + dom_settle_timeout: Timeout in ms to wait for DOM to settle + + local_browser_launch_options: Options for local browser launch + + model: AI model to use for actions + + project_id: Project ID for Browserbase (required when env=BROWSERBASE) + + self_heal: Enable self-healing for failed actions + + system_prompt: Custom system prompt for AI actions + + verbose: Logging verbosity level + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return await self._post( + "/sessions/start", + body=await async_maybe_transform( + { + "env": env, + "api_key": api_key, + "dom_settle_timeout": dom_settle_timeout, + "local_browser_launch_options": local_browser_launch_options, + "model": model, + "project_id": project_id, + "self_heal": self_heal, + "system_prompt": system_prompt, + "verbose": verbose, + }, + session_start_params.SessionStartParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=SessionStartResponse, + ) + + +class SessionsResourceWithRawResponse: + def __init__(self, sessions: SessionsResource) -> None: + self._sessions = sessions + + self.act = to_raw_response_wrapper( + sessions.act, + ) + self.end = to_raw_response_wrapper( + sessions.end, + ) + self.execute_agent = to_raw_response_wrapper( + sessions.execute_agent, + ) + self.extract = to_raw_response_wrapper( + sessions.extract, + ) + self.navigate = to_raw_response_wrapper( + sessions.navigate, + ) + self.observe = to_raw_response_wrapper( + sessions.observe, + ) + self.start = to_raw_response_wrapper( + sessions.start, + ) + + +class AsyncSessionsResourceWithRawResponse: + def __init__(self, sessions: AsyncSessionsResource) -> None: + self._sessions = sessions + + self.act = async_to_raw_response_wrapper( + sessions.act, + ) + self.end = async_to_raw_response_wrapper( + sessions.end, + ) + self.execute_agent = async_to_raw_response_wrapper( + sessions.execute_agent, + ) + self.extract = async_to_raw_response_wrapper( + sessions.extract, + ) + self.navigate = async_to_raw_response_wrapper( + sessions.navigate, + ) + self.observe = async_to_raw_response_wrapper( + sessions.observe, + ) + self.start = async_to_raw_response_wrapper( + sessions.start, + ) + + +class SessionsResourceWithStreamingResponse: + def __init__(self, sessions: SessionsResource) -> None: + self._sessions = sessions + + self.act = to_streamed_response_wrapper( + sessions.act, + ) + self.end = to_streamed_response_wrapper( + sessions.end, + ) + self.execute_agent = to_streamed_response_wrapper( + sessions.execute_agent, + ) + self.extract = to_streamed_response_wrapper( + sessions.extract, + ) + self.navigate = to_streamed_response_wrapper( + sessions.navigate, + ) + self.observe = to_streamed_response_wrapper( + sessions.observe, + ) + self.start = to_streamed_response_wrapper( + sessions.start, + ) + + +class AsyncSessionsResourceWithStreamingResponse: + def __init__(self, sessions: AsyncSessionsResource) -> None: + self._sessions = sessions + + self.act = async_to_streamed_response_wrapper( + sessions.act, + ) + self.end = async_to_streamed_response_wrapper( + sessions.end, + ) + self.execute_agent = async_to_streamed_response_wrapper( + sessions.execute_agent, + ) + self.extract = async_to_streamed_response_wrapper( + sessions.extract, + ) + self.navigate = async_to_streamed_response_wrapper( + sessions.navigate, + ) + self.observe = async_to_streamed_response_wrapper( + sessions.observe, + ) + self.start = async_to_streamed_response_wrapper( + sessions.start, + ) diff --git a/src/stagehand/types/__init__.py b/src/stagehand/types/__init__.py new file mode 100644 index 00000000..077ed5ad --- /dev/null +++ b/src/stagehand/types/__init__.py @@ -0,0 +1,20 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from .action import Action as Action +from .action_param import ActionParam as ActionParam +from .model_config_param import ModelConfigParam as ModelConfigParam +from .session_act_params import SessionActParams as SessionActParams +from .session_act_response import SessionActResponse as SessionActResponse +from .session_end_response import SessionEndResponse as SessionEndResponse +from .session_start_params import SessionStartParams as SessionStartParams +from .session_extract_params import SessionExtractParams as SessionExtractParams +from .session_observe_params import SessionObserveParams as SessionObserveParams +from .session_start_response import SessionStartResponse as SessionStartResponse +from .session_navigate_params import SessionNavigateParams as SessionNavigateParams +from .session_extract_response import SessionExtractResponse as SessionExtractResponse +from .session_observe_response import SessionObserveResponse as SessionObserveResponse +from .session_navigate_response import SessionNavigateResponse as SessionNavigateResponse +from .session_execute_agent_params import SessionExecuteAgentParams as SessionExecuteAgentParams +from .session_execute_agent_response import SessionExecuteAgentResponse as SessionExecuteAgentResponse diff --git a/src/stagehand/types/action.py b/src/stagehand/types/action.py new file mode 100644 index 00000000..c68f9aaf --- /dev/null +++ b/src/stagehand/types/action.py @@ -0,0 +1,26 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List, Optional + +from pydantic import Field as FieldInfo + +from .._models import BaseModel + +__all__ = ["Action"] + + +class Action(BaseModel): + arguments: List[str] + """Arguments for the method""" + + description: str + """Human-readable description of the action""" + + method: str + """Method to execute (e.g., "click", "fill")""" + + selector: str + """CSS or XPath selector for the element""" + + backend_node_id: Optional[int] = FieldInfo(alias="backendNodeId", default=None) + """CDP backend node ID""" diff --git a/src/stagehand/types/action_param.py b/src/stagehand/types/action_param.py new file mode 100644 index 00000000..1a0cc408 --- /dev/null +++ b/src/stagehand/types/action_param.py @@ -0,0 +1,27 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Required, Annotated, TypedDict + +from .._types import SequenceNotStr +from .._utils import PropertyInfo + +__all__ = ["ActionParam"] + + +class ActionParam(TypedDict, total=False): + arguments: Required[SequenceNotStr[str]] + """Arguments for the method""" + + description: Required[str] + """Human-readable description of the action""" + + method: Required[str] + """Method to execute (e.g., "click", "fill")""" + + selector: Required[str] + """CSS or XPath selector for the element""" + + backend_node_id: Annotated[int, PropertyInfo(alias="backendNodeId")] + """CDP backend node ID""" diff --git a/src/stagehand/types/model_config_param.py b/src/stagehand/types/model_config_param.py new file mode 100644 index 00000000..2cd1e97d --- /dev/null +++ b/src/stagehand/types/model_config_param.py @@ -0,0 +1,22 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Literal, Annotated, TypedDict + +from .._utils import PropertyInfo + +__all__ = ["ModelConfigParam"] + + +class ModelConfigParam(TypedDict, total=False): + api_key: Annotated[str, PropertyInfo(alias="apiKey")] + """API key for the model provider""" + + base_url: Annotated[str, PropertyInfo(alias="baseURL")] + """Custom base URL for API""" + + model: str + """Model name""" + + provider: Literal["openai", "anthropic", "google"] diff --git a/src/stagehand/types/session_act_params.py b/src/stagehand/types/session_act_params.py new file mode 100644 index 00000000..80e60817 --- /dev/null +++ b/src/stagehand/types/session_act_params.py @@ -0,0 +1,37 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Dict, Union +from typing_extensions import Literal, Required, Annotated, TypeAlias, TypedDict + +from .._utils import PropertyInfo +from .action_param import ActionParam +from .model_config_param import ModelConfigParam + +__all__ = ["SessionActParams", "Input", "Options"] + + +class SessionActParams(TypedDict, total=False): + input: Required[Input] + """Natural language instruction""" + + frame_id: Annotated[str, PropertyInfo(alias="frameId")] + """Frame ID to act on (optional)""" + + options: Options + + x_stream_response: Annotated[Literal["true", "false"], PropertyInfo(alias="x-stream-response")] + + +Input: TypeAlias = Union[str, ActionParam] + + +class Options(TypedDict, total=False): + model: ModelConfigParam + + timeout: int + """Timeout in milliseconds""" + + variables: Dict[str, str] + """Template variables for instruction""" diff --git a/src/stagehand/types/session_act_response.py b/src/stagehand/types/session_act_response.py new file mode 100644 index 00000000..7a99253f --- /dev/null +++ b/src/stagehand/types/session_act_response.py @@ -0,0 +1,19 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List + +from .action import Action +from .._models import BaseModel + +__all__ = ["SessionActResponse"] + + +class SessionActResponse(BaseModel): + actions: List[Action] + """Actions that were executed""" + + message: str + """Result message""" + + success: bool + """Whether the action succeeded""" diff --git a/src/stagehand/types/session_end_response.py b/src/stagehand/types/session_end_response.py new file mode 100644 index 00000000..d088e6ee --- /dev/null +++ b/src/stagehand/types/session_end_response.py @@ -0,0 +1,11 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional + +from .._models import BaseModel + +__all__ = ["SessionEndResponse"] + + +class SessionEndResponse(BaseModel): + success: Optional[bool] = None diff --git a/src/stagehand/types/session_execute_agent_params.py b/src/stagehand/types/session_execute_agent_params.py new file mode 100644 index 00000000..c3493805 --- /dev/null +++ b/src/stagehand/types/session_execute_agent_params.py @@ -0,0 +1,46 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Union +from typing_extensions import Literal, Required, Annotated, TypeAlias, TypedDict + +from .._utils import PropertyInfo +from .model_config_param import ModelConfigParam + +__all__ = ["SessionExecuteAgentParams", "AgentConfig", "AgentConfigModel", "ExecuteOptions"] + + +class SessionExecuteAgentParams(TypedDict, total=False): + agent_config: Required[Annotated[AgentConfig, PropertyInfo(alias="agentConfig")]] + + execute_options: Required[Annotated[ExecuteOptions, PropertyInfo(alias="executeOptions")]] + + frame_id: Annotated[str, PropertyInfo(alias="frameId")] + + x_stream_response: Annotated[Literal["true", "false"], PropertyInfo(alias="x-stream-response")] + + +AgentConfigModel: TypeAlias = Union[str, ModelConfigParam] + + +class AgentConfig(TypedDict, total=False): + cua: bool + """Enable Computer Use Agent mode""" + + model: AgentConfigModel + + provider: Literal["openai", "anthropic", "google"] + + system_prompt: Annotated[str, PropertyInfo(alias="systemPrompt")] + + +class ExecuteOptions(TypedDict, total=False): + instruction: Required[str] + """Task for the agent to complete""" + + highlight_cursor: Annotated[bool, PropertyInfo(alias="highlightCursor")] + """Visually highlight the cursor during actions""" + + max_steps: Annotated[int, PropertyInfo(alias="maxSteps")] + """Maximum number of steps the agent can take""" diff --git a/src/stagehand/types/session_execute_agent_response.py b/src/stagehand/types/session_execute_agent_response.py new file mode 100644 index 00000000..3fa53390 --- /dev/null +++ b/src/stagehand/types/session_execute_agent_response.py @@ -0,0 +1,15 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List, Optional + +from .._models import BaseModel + +__all__ = ["SessionExecuteAgentResponse"] + + +class SessionExecuteAgentResponse(BaseModel): + message: Optional[str] = None + """Final message from the agent""" + + steps: Optional[List[object]] = None + """Steps taken by the agent""" diff --git a/src/stagehand/types/session_extract_params.py b/src/stagehand/types/session_extract_params.py new file mode 100644 index 00000000..05005193 --- /dev/null +++ b/src/stagehand/types/session_extract_params.py @@ -0,0 +1,35 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Dict +from typing_extensions import Literal, Annotated, TypedDict + +from .._utils import PropertyInfo +from .model_config_param import ModelConfigParam + +__all__ = ["SessionExtractParams", "Options"] + + +class SessionExtractParams(TypedDict, total=False): + frame_id: Annotated[str, PropertyInfo(alias="frameId")] + """Frame ID to extract from""" + + instruction: str + """Natural language instruction for extraction""" + + options: Options + + schema: Dict[str, object] + """JSON Schema for structured output""" + + x_stream_response: Annotated[Literal["true", "false"], PropertyInfo(alias="x-stream-response")] + + +class Options(TypedDict, total=False): + model: ModelConfigParam + + selector: str + """Extract only from elements matching this selector""" + + timeout: int diff --git a/src/stagehand/types/session_extract_response.py b/src/stagehand/types/session_extract_response.py new file mode 100644 index 00000000..723ed97c --- /dev/null +++ b/src/stagehand/types/session_extract_response.py @@ -0,0 +1,17 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Dict, Union, Optional +from typing_extensions import TypeAlias + +from .._models import BaseModel + +__all__ = ["SessionExtractResponse", "Extraction"] + + +class Extraction(BaseModel): + """Default extraction result""" + + extraction: Optional[str] = None + + +SessionExtractResponse: TypeAlias = Union[Extraction, Dict[str, object]] diff --git a/src/stagehand/types/session_navigate_params.py b/src/stagehand/types/session_navigate_params.py new file mode 100644 index 00000000..a96eacfc --- /dev/null +++ b/src/stagehand/types/session_navigate_params.py @@ -0,0 +1,25 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Literal, Required, Annotated, TypedDict + +from .._utils import PropertyInfo + +__all__ = ["SessionNavigateParams", "Options"] + + +class SessionNavigateParams(TypedDict, total=False): + url: Required[str] + """URL to navigate to""" + + frame_id: Annotated[str, PropertyInfo(alias="frameId")] + + options: Options + + x_stream_response: Annotated[Literal["true", "false"], PropertyInfo(alias="x-stream-response")] + + +class Options(TypedDict, total=False): + wait_until: Annotated[Literal["load", "domcontentloaded", "networkidle"], PropertyInfo(alias="waitUntil")] + """When to consider navigation complete""" diff --git a/src/stagehand/types/session_navigate_response.py b/src/stagehand/types/session_navigate_response.py new file mode 100644 index 00000000..d7422b9f --- /dev/null +++ b/src/stagehand/types/session_navigate_response.py @@ -0,0 +1,17 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional + +from .._models import BaseModel + +__all__ = ["SessionNavigateResponse"] + + +class SessionNavigateResponse(BaseModel): + """Navigation response (may be null)""" + + ok: Optional[bool] = None + + status: Optional[int] = None + + url: Optional[str] = None diff --git a/src/stagehand/types/session_observe_params.py b/src/stagehand/types/session_observe_params.py new file mode 100644 index 00000000..c6b8a777 --- /dev/null +++ b/src/stagehand/types/session_observe_params.py @@ -0,0 +1,31 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Literal, Annotated, TypedDict + +from .._utils import PropertyInfo +from .model_config_param import ModelConfigParam + +__all__ = ["SessionObserveParams", "Options"] + + +class SessionObserveParams(TypedDict, total=False): + frame_id: Annotated[str, PropertyInfo(alias="frameId")] + """Frame ID to observe""" + + instruction: str + """Natural language instruction to filter actions""" + + options: Options + + x_stream_response: Annotated[Literal["true", "false"], PropertyInfo(alias="x-stream-response")] + + +class Options(TypedDict, total=False): + model: ModelConfigParam + + selector: str + """Observe only elements matching this selector""" + + timeout: int diff --git a/src/stagehand/types/session_observe_response.py b/src/stagehand/types/session_observe_response.py new file mode 100644 index 00000000..fd8b2fbe --- /dev/null +++ b/src/stagehand/types/session_observe_response.py @@ -0,0 +1,10 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List +from typing_extensions import TypeAlias + +from .action import Action + +__all__ = ["SessionObserveResponse"] + +SessionObserveResponse: TypeAlias = List[Action] diff --git a/src/stagehand/types/session_start_params.py b/src/stagehand/types/session_start_params.py new file mode 100644 index 00000000..fc7573c1 --- /dev/null +++ b/src/stagehand/types/session_start_params.py @@ -0,0 +1,44 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Literal, Required, Annotated, TypedDict + +from .._utils import PropertyInfo + +__all__ = ["SessionStartParams", "LocalBrowserLaunchOptions"] + + +class SessionStartParams(TypedDict, total=False): + env: Required[Literal["LOCAL", "BROWSERBASE"]] + """Environment to run the browser in""" + + api_key: Annotated[str, PropertyInfo(alias="apiKey")] + """API key for Browserbase (required when env=BROWSERBASE)""" + + dom_settle_timeout: Annotated[int, PropertyInfo(alias="domSettleTimeout")] + """Timeout in ms to wait for DOM to settle""" + + local_browser_launch_options: Annotated[LocalBrowserLaunchOptions, PropertyInfo(alias="localBrowserLaunchOptions")] + """Options for local browser launch""" + + model: str + """AI model to use for actions""" + + project_id: Annotated[str, PropertyInfo(alias="projectId")] + """Project ID for Browserbase (required when env=BROWSERBASE)""" + + self_heal: Annotated[bool, PropertyInfo(alias="selfHeal")] + """Enable self-healing for failed actions""" + + system_prompt: Annotated[str, PropertyInfo(alias="systemPrompt")] + """Custom system prompt for AI actions""" + + verbose: int + """Logging verbosity level""" + + +class LocalBrowserLaunchOptions(TypedDict, total=False): + """Options for local browser launch""" + + headless: bool diff --git a/src/stagehand/types/session_start_response.py b/src/stagehand/types/session_start_response.py new file mode 100644 index 00000000..b5862c8b --- /dev/null +++ b/src/stagehand/types/session_start_response.py @@ -0,0 +1,15 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from pydantic import Field as FieldInfo + +from .._models import BaseModel + +__all__ = ["SessionStartResponse"] + + +class SessionStartResponse(BaseModel): + available: bool + """Whether the session is ready to use""" + + session_id: str = FieldInfo(alias="sessionId") + """Unique identifier for the session""" diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 00000000..fd8019a9 --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. diff --git a/tests/api_resources/__init__.py b/tests/api_resources/__init__.py new file mode 100644 index 00000000..fd8019a9 --- /dev/null +++ b/tests/api_resources/__init__.py @@ -0,0 +1 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. diff --git a/tests/api_resources/test_sessions.py b/tests/api_resources/test_sessions.py new file mode 100644 index 00000000..2c0829d5 --- /dev/null +++ b/tests/api_resources/test_sessions.py @@ -0,0 +1,862 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import os +from typing import Any, Optional, cast + +import pytest + +from stagehand import Stagehand, AsyncStagehand +from tests.utils import assert_matches_type +from stagehand.types import ( + SessionActResponse, + SessionEndResponse, + SessionStartResponse, + SessionExtractResponse, + SessionObserveResponse, + SessionNavigateResponse, + SessionExecuteAgentResponse, +) + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + + +class TestSessions: + parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_act(self, client: Stagehand) -> None: + session = client.sessions.act( + session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + input="click the sign in button", + ) + assert_matches_type(SessionActResponse, session, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_act_with_all_params(self, client: Stagehand) -> None: + session = client.sessions.act( + session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + input="click the sign in button", + frame_id="frameId", + options={ + "model": { + "api_key": "apiKey", + "base_url": "https://example.com", + "model": "model", + "provider": "openai", + }, + "timeout": 0, + "variables": {"foo": "string"}, + }, + x_stream_response="true", + ) + assert_matches_type(SessionActResponse, session, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_raw_response_act(self, client: Stagehand) -> None: + response = client.sessions.with_raw_response.act( + session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + input="click the sign in button", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + session = response.parse() + assert_matches_type(SessionActResponse, session, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_streaming_response_act(self, client: Stagehand) -> None: + with client.sessions.with_streaming_response.act( + session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + input="click the sign in button", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + session = response.parse() + assert_matches_type(SessionActResponse, session, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_path_params_act(self, client: Stagehand) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `session_id` but received ''"): + client.sessions.with_raw_response.act( + session_id="", + input="click the sign in button", + ) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_end(self, client: Stagehand) -> None: + session = client.sessions.end( + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + assert_matches_type(SessionEndResponse, session, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_raw_response_end(self, client: Stagehand) -> None: + response = client.sessions.with_raw_response.end( + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + session = response.parse() + assert_matches_type(SessionEndResponse, session, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_streaming_response_end(self, client: Stagehand) -> None: + with client.sessions.with_streaming_response.end( + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + session = response.parse() + assert_matches_type(SessionEndResponse, session, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_path_params_end(self, client: Stagehand) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `session_id` but received ''"): + client.sessions.with_raw_response.end( + "", + ) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_execute_agent(self, client: Stagehand) -> None: + session = client.sessions.execute_agent( + session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + agent_config={}, + execute_options={"instruction": "Find and click the first product"}, + ) + assert_matches_type(SessionExecuteAgentResponse, session, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_execute_agent_with_all_params(self, client: Stagehand) -> None: + session = client.sessions.execute_agent( + session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + agent_config={ + "cua": True, + "model": "openai/gpt-4o", + "provider": "openai", + "system_prompt": "systemPrompt", + }, + execute_options={ + "instruction": "Find and click the first product", + "highlight_cursor": True, + "max_steps": 10, + }, + frame_id="frameId", + x_stream_response="true", + ) + assert_matches_type(SessionExecuteAgentResponse, session, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_raw_response_execute_agent(self, client: Stagehand) -> None: + response = client.sessions.with_raw_response.execute_agent( + session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + agent_config={}, + execute_options={"instruction": "Find and click the first product"}, + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + session = response.parse() + assert_matches_type(SessionExecuteAgentResponse, session, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_streaming_response_execute_agent(self, client: Stagehand) -> None: + with client.sessions.with_streaming_response.execute_agent( + session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + agent_config={}, + execute_options={"instruction": "Find and click the first product"}, + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + session = response.parse() + assert_matches_type(SessionExecuteAgentResponse, session, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_path_params_execute_agent(self, client: Stagehand) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `session_id` but received ''"): + client.sessions.with_raw_response.execute_agent( + session_id="", + agent_config={}, + execute_options={"instruction": "Find and click the first product"}, + ) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_extract(self, client: Stagehand) -> None: + session = client.sessions.extract( + session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + assert_matches_type(SessionExtractResponse, session, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_extract_with_all_params(self, client: Stagehand) -> None: + session = client.sessions.extract( + session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + frame_id="frameId", + instruction="extract the page title", + options={ + "model": { + "api_key": "apiKey", + "base_url": "https://example.com", + "model": "model", + "provider": "openai", + }, + "selector": "selector", + "timeout": 0, + }, + schema={"foo": "bar"}, + x_stream_response="true", + ) + assert_matches_type(SessionExtractResponse, session, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_raw_response_extract(self, client: Stagehand) -> None: + response = client.sessions.with_raw_response.extract( + session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + session = response.parse() + assert_matches_type(SessionExtractResponse, session, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_streaming_response_extract(self, client: Stagehand) -> None: + with client.sessions.with_streaming_response.extract( + session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + session = response.parse() + assert_matches_type(SessionExtractResponse, session, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_path_params_extract(self, client: Stagehand) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `session_id` but received ''"): + client.sessions.with_raw_response.extract( + session_id="", + ) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_navigate(self, client: Stagehand) -> None: + session = client.sessions.navigate( + session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + url="https://example.com", + ) + assert_matches_type(Optional[SessionNavigateResponse], session, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_navigate_with_all_params(self, client: Stagehand) -> None: + session = client.sessions.navigate( + session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + url="https://example.com", + frame_id="frameId", + options={"wait_until": "load"}, + x_stream_response="true", + ) + assert_matches_type(Optional[SessionNavigateResponse], session, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_raw_response_navigate(self, client: Stagehand) -> None: + response = client.sessions.with_raw_response.navigate( + session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + url="https://example.com", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + session = response.parse() + assert_matches_type(Optional[SessionNavigateResponse], session, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_streaming_response_navigate(self, client: Stagehand) -> None: + with client.sessions.with_streaming_response.navigate( + session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + url="https://example.com", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + session = response.parse() + assert_matches_type(Optional[SessionNavigateResponse], session, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_path_params_navigate(self, client: Stagehand) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `session_id` but received ''"): + client.sessions.with_raw_response.navigate( + session_id="", + url="https://example.com", + ) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_observe(self, client: Stagehand) -> None: + session = client.sessions.observe( + session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + assert_matches_type(SessionObserveResponse, session, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_observe_with_all_params(self, client: Stagehand) -> None: + session = client.sessions.observe( + session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + frame_id="frameId", + instruction="instruction", + options={ + "model": { + "api_key": "apiKey", + "base_url": "https://example.com", + "model": "model", + "provider": "openai", + }, + "selector": "selector", + "timeout": 0, + }, + x_stream_response="true", + ) + assert_matches_type(SessionObserveResponse, session, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_raw_response_observe(self, client: Stagehand) -> None: + response = client.sessions.with_raw_response.observe( + session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + session = response.parse() + assert_matches_type(SessionObserveResponse, session, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_streaming_response_observe(self, client: Stagehand) -> None: + with client.sessions.with_streaming_response.observe( + session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + session = response.parse() + assert_matches_type(SessionObserveResponse, session, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_path_params_observe(self, client: Stagehand) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `session_id` but received ''"): + client.sessions.with_raw_response.observe( + session_id="", + ) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_start(self, client: Stagehand) -> None: + session = client.sessions.start( + env="LOCAL", + ) + assert_matches_type(SessionStartResponse, session, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_start_with_all_params(self, client: Stagehand) -> None: + session = client.sessions.start( + env="LOCAL", + api_key="apiKey", + dom_settle_timeout=0, + local_browser_launch_options={"headless": True}, + model="openai/gpt-4o", + project_id="projectId", + self_heal=True, + system_prompt="systemPrompt", + verbose=1, + ) + assert_matches_type(SessionStartResponse, session, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_raw_response_start(self, client: Stagehand) -> None: + response = client.sessions.with_raw_response.start( + env="LOCAL", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + session = response.parse() + assert_matches_type(SessionStartResponse, session, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_streaming_response_start(self, client: Stagehand) -> None: + with client.sessions.with_streaming_response.start( + env="LOCAL", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + session = response.parse() + assert_matches_type(SessionStartResponse, session, path=["response"]) + + assert cast(Any, response.is_closed) is True + + +class TestAsyncSessions: + parametrize = pytest.mark.parametrize( + "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] + ) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_method_act(self, async_client: AsyncStagehand) -> None: + session = await async_client.sessions.act( + session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + input="click the sign in button", + ) + assert_matches_type(SessionActResponse, session, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_method_act_with_all_params(self, async_client: AsyncStagehand) -> None: + session = await async_client.sessions.act( + session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + input="click the sign in button", + frame_id="frameId", + options={ + "model": { + "api_key": "apiKey", + "base_url": "https://example.com", + "model": "model", + "provider": "openai", + }, + "timeout": 0, + "variables": {"foo": "string"}, + }, + x_stream_response="true", + ) + assert_matches_type(SessionActResponse, session, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_raw_response_act(self, async_client: AsyncStagehand) -> None: + response = await async_client.sessions.with_raw_response.act( + session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + input="click the sign in button", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + session = await response.parse() + assert_matches_type(SessionActResponse, session, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_streaming_response_act(self, async_client: AsyncStagehand) -> None: + async with async_client.sessions.with_streaming_response.act( + session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + input="click the sign in button", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + session = await response.parse() + assert_matches_type(SessionActResponse, session, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_path_params_act(self, async_client: AsyncStagehand) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `session_id` but received ''"): + await async_client.sessions.with_raw_response.act( + session_id="", + input="click the sign in button", + ) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_method_end(self, async_client: AsyncStagehand) -> None: + session = await async_client.sessions.end( + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + assert_matches_type(SessionEndResponse, session, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_raw_response_end(self, async_client: AsyncStagehand) -> None: + response = await async_client.sessions.with_raw_response.end( + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + session = await response.parse() + assert_matches_type(SessionEndResponse, session, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_streaming_response_end(self, async_client: AsyncStagehand) -> None: + async with async_client.sessions.with_streaming_response.end( + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + session = await response.parse() + assert_matches_type(SessionEndResponse, session, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_path_params_end(self, async_client: AsyncStagehand) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `session_id` but received ''"): + await async_client.sessions.with_raw_response.end( + "", + ) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_method_execute_agent(self, async_client: AsyncStagehand) -> None: + session = await async_client.sessions.execute_agent( + session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + agent_config={}, + execute_options={"instruction": "Find and click the first product"}, + ) + assert_matches_type(SessionExecuteAgentResponse, session, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_method_execute_agent_with_all_params(self, async_client: AsyncStagehand) -> None: + session = await async_client.sessions.execute_agent( + session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + agent_config={ + "cua": True, + "model": "openai/gpt-4o", + "provider": "openai", + "system_prompt": "systemPrompt", + }, + execute_options={ + "instruction": "Find and click the first product", + "highlight_cursor": True, + "max_steps": 10, + }, + frame_id="frameId", + x_stream_response="true", + ) + assert_matches_type(SessionExecuteAgentResponse, session, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_raw_response_execute_agent(self, async_client: AsyncStagehand) -> None: + response = await async_client.sessions.with_raw_response.execute_agent( + session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + agent_config={}, + execute_options={"instruction": "Find and click the first product"}, + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + session = await response.parse() + assert_matches_type(SessionExecuteAgentResponse, session, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_streaming_response_execute_agent(self, async_client: AsyncStagehand) -> None: + async with async_client.sessions.with_streaming_response.execute_agent( + session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + agent_config={}, + execute_options={"instruction": "Find and click the first product"}, + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + session = await response.parse() + assert_matches_type(SessionExecuteAgentResponse, session, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_path_params_execute_agent(self, async_client: AsyncStagehand) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `session_id` but received ''"): + await async_client.sessions.with_raw_response.execute_agent( + session_id="", + agent_config={}, + execute_options={"instruction": "Find and click the first product"}, + ) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_method_extract(self, async_client: AsyncStagehand) -> None: + session = await async_client.sessions.extract( + session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + assert_matches_type(SessionExtractResponse, session, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_method_extract_with_all_params(self, async_client: AsyncStagehand) -> None: + session = await async_client.sessions.extract( + session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + frame_id="frameId", + instruction="extract the page title", + options={ + "model": { + "api_key": "apiKey", + "base_url": "https://example.com", + "model": "model", + "provider": "openai", + }, + "selector": "selector", + "timeout": 0, + }, + schema={"foo": "bar"}, + x_stream_response="true", + ) + assert_matches_type(SessionExtractResponse, session, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_raw_response_extract(self, async_client: AsyncStagehand) -> None: + response = await async_client.sessions.with_raw_response.extract( + session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + session = await response.parse() + assert_matches_type(SessionExtractResponse, session, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_streaming_response_extract(self, async_client: AsyncStagehand) -> None: + async with async_client.sessions.with_streaming_response.extract( + session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + session = await response.parse() + assert_matches_type(SessionExtractResponse, session, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_path_params_extract(self, async_client: AsyncStagehand) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `session_id` but received ''"): + await async_client.sessions.with_raw_response.extract( + session_id="", + ) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_method_navigate(self, async_client: AsyncStagehand) -> None: + session = await async_client.sessions.navigate( + session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + url="https://example.com", + ) + assert_matches_type(Optional[SessionNavigateResponse], session, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_method_navigate_with_all_params(self, async_client: AsyncStagehand) -> None: + session = await async_client.sessions.navigate( + session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + url="https://example.com", + frame_id="frameId", + options={"wait_until": "load"}, + x_stream_response="true", + ) + assert_matches_type(Optional[SessionNavigateResponse], session, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_raw_response_navigate(self, async_client: AsyncStagehand) -> None: + response = await async_client.sessions.with_raw_response.navigate( + session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + url="https://example.com", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + session = await response.parse() + assert_matches_type(Optional[SessionNavigateResponse], session, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_streaming_response_navigate(self, async_client: AsyncStagehand) -> None: + async with async_client.sessions.with_streaming_response.navigate( + session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + url="https://example.com", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + session = await response.parse() + assert_matches_type(Optional[SessionNavigateResponse], session, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_path_params_navigate(self, async_client: AsyncStagehand) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `session_id` but received ''"): + await async_client.sessions.with_raw_response.navigate( + session_id="", + url="https://example.com", + ) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_method_observe(self, async_client: AsyncStagehand) -> None: + session = await async_client.sessions.observe( + session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + assert_matches_type(SessionObserveResponse, session, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_method_observe_with_all_params(self, async_client: AsyncStagehand) -> None: + session = await async_client.sessions.observe( + session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + frame_id="frameId", + instruction="instruction", + options={ + "model": { + "api_key": "apiKey", + "base_url": "https://example.com", + "model": "model", + "provider": "openai", + }, + "selector": "selector", + "timeout": 0, + }, + x_stream_response="true", + ) + assert_matches_type(SessionObserveResponse, session, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_raw_response_observe(self, async_client: AsyncStagehand) -> None: + response = await async_client.sessions.with_raw_response.observe( + session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + session = await response.parse() + assert_matches_type(SessionObserveResponse, session, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_streaming_response_observe(self, async_client: AsyncStagehand) -> None: + async with async_client.sessions.with_streaming_response.observe( + session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + session = await response.parse() + assert_matches_type(SessionObserveResponse, session, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_path_params_observe(self, async_client: AsyncStagehand) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `session_id` but received ''"): + await async_client.sessions.with_raw_response.observe( + session_id="", + ) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_method_start(self, async_client: AsyncStagehand) -> None: + session = await async_client.sessions.start( + env="LOCAL", + ) + assert_matches_type(SessionStartResponse, session, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_method_start_with_all_params(self, async_client: AsyncStagehand) -> None: + session = await async_client.sessions.start( + env="LOCAL", + api_key="apiKey", + dom_settle_timeout=0, + local_browser_launch_options={"headless": True}, + model="openai/gpt-4o", + project_id="projectId", + self_heal=True, + system_prompt="systemPrompt", + verbose=1, + ) + assert_matches_type(SessionStartResponse, session, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_raw_response_start(self, async_client: AsyncStagehand) -> None: + response = await async_client.sessions.with_raw_response.start( + env="LOCAL", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + session = await response.parse() + assert_matches_type(SessionStartResponse, session, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_streaming_response_start(self, async_client: AsyncStagehand) -> None: + async with async_client.sessions.with_streaming_response.start( + env="LOCAL", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + session = await response.parse() + assert_matches_type(SessionStartResponse, session, path=["response"]) + + assert cast(Any, response.is_closed) is True diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 00000000..f57b7286 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,84 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import os +import logging +from typing import TYPE_CHECKING, Iterator, AsyncIterator + +import httpx +import pytest +from pytest_asyncio import is_async_test + +from stagehand import Stagehand, AsyncStagehand, DefaultAioHttpClient +from stagehand._utils import is_dict + +if TYPE_CHECKING: + from _pytest.fixtures import FixtureRequest # pyright: ignore[reportPrivateImportUsage] + +pytest.register_assert_rewrite("tests.utils") + +logging.getLogger("stagehand").setLevel(logging.DEBUG) + + +# automatically add `pytest.mark.asyncio()` to all of our async tests +# so we don't have to add that boilerplate everywhere +def pytest_collection_modifyitems(items: list[pytest.Function]) -> None: + pytest_asyncio_tests = (item for item in items if is_async_test(item)) + session_scope_marker = pytest.mark.asyncio(loop_scope="session") + for async_test in pytest_asyncio_tests: + async_test.add_marker(session_scope_marker, append=False) + + # We skip tests that use both the aiohttp client and respx_mock as respx_mock + # doesn't support custom transports. + for item in items: + if "async_client" not in item.fixturenames or "respx_mock" not in item.fixturenames: + continue + + if not hasattr(item, "callspec"): + continue + + async_client_param = item.callspec.params.get("async_client") + if is_dict(async_client_param) and async_client_param.get("http_client") == "aiohttp": + item.add_marker(pytest.mark.skip(reason="aiohttp client is not compatible with respx_mock")) + + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + +api_key = "My API Key" + + +@pytest.fixture(scope="session") +def client(request: FixtureRequest) -> Iterator[Stagehand]: + strict = getattr(request, "param", True) + if not isinstance(strict, bool): + raise TypeError(f"Unexpected fixture parameter type {type(strict)}, expected {bool}") + + with Stagehand(base_url=base_url, api_key=api_key, _strict_response_validation=strict) as client: + yield client + + +@pytest.fixture(scope="session") +async def async_client(request: FixtureRequest) -> AsyncIterator[AsyncStagehand]: + param = getattr(request, "param", True) + + # defaults + strict = True + http_client: None | httpx.AsyncClient = None + + if isinstance(param, bool): + strict = param + elif is_dict(param): + strict = param.get("strict", True) + assert isinstance(strict, bool) + + http_client_type = param.get("http_client", "httpx") + if http_client_type == "aiohttp": + http_client = DefaultAioHttpClient() + else: + raise TypeError(f"Unexpected fixture parameter type {type(param)}, expected bool or dict") + + async with AsyncStagehand( + base_url=base_url, api_key=api_key, _strict_response_validation=strict, http_client=http_client + ) as client: + yield client diff --git a/tests/sample_file.txt b/tests/sample_file.txt new file mode 100644 index 00000000..af5626b4 --- /dev/null +++ b/tests/sample_file.txt @@ -0,0 +1 @@ +Hello, world! diff --git a/tests/test_client.py b/tests/test_client.py new file mode 100644 index 00000000..b750b99f --- /dev/null +++ b/tests/test_client.py @@ -0,0 +1,1765 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import gc +import os +import sys +import json +import asyncio +import inspect +import tracemalloc +from typing import Any, Union, cast +from unittest import mock +from typing_extensions import Literal + +import httpx +import pytest +from respx import MockRouter +from pydantic import ValidationError + +from stagehand import Stagehand, AsyncStagehand, APIResponseValidationError +from stagehand._types import Omit +from stagehand._utils import asyncify +from stagehand._models import BaseModel, FinalRequestOptions +from stagehand._exceptions import APIStatusError, APITimeoutError, APIResponseValidationError +from stagehand._base_client import ( + DEFAULT_TIMEOUT, + HTTPX_DEFAULT_TIMEOUT, + BaseClient, + OtherPlatform, + DefaultHttpxClient, + DefaultAsyncHttpxClient, + get_platform, + make_request_options, +) + +from .utils import update_env + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") +api_key = "My API Key" + + +def _get_params(client: BaseClient[Any, Any]) -> dict[str, str]: + request = client._build_request(FinalRequestOptions(method="get", url="/foo")) + url = httpx.URL(request.url) + return dict(url.params) + + +def _low_retry_timeout(*_args: Any, **_kwargs: Any) -> float: + return 0.1 + + +def _get_open_connections(client: Stagehand | AsyncStagehand) -> int: + transport = client._client._transport + assert isinstance(transport, httpx.HTTPTransport) or isinstance(transport, httpx.AsyncHTTPTransport) + + pool = transport._pool + return len(pool._requests) + + +class TestStagehand: + @pytest.mark.respx(base_url=base_url) + def test_raw_response(self, respx_mock: MockRouter, client: Stagehand) -> None: + respx_mock.post("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"})) + + response = client.post("/foo", cast_to=httpx.Response) + assert response.status_code == 200 + assert isinstance(response, httpx.Response) + assert response.json() == {"foo": "bar"} + + @pytest.mark.respx(base_url=base_url) + def test_raw_response_for_binary(self, respx_mock: MockRouter, client: Stagehand) -> None: + respx_mock.post("/foo").mock( + return_value=httpx.Response(200, headers={"Content-Type": "application/binary"}, content='{"foo": "bar"}') + ) + + response = client.post("/foo", cast_to=httpx.Response) + assert response.status_code == 200 + assert isinstance(response, httpx.Response) + assert response.json() == {"foo": "bar"} + + def test_copy(self, client: Stagehand) -> None: + copied = client.copy() + assert id(copied) != id(client) + + copied = client.copy(api_key="another My API Key") + assert copied.api_key == "another My API Key" + assert client.api_key == "My API Key" + + def test_copy_default_options(self, client: Stagehand) -> None: + # options that have a default are overridden correctly + copied = client.copy(max_retries=7) + assert copied.max_retries == 7 + assert client.max_retries == 2 + + copied2 = copied.copy(max_retries=6) + assert copied2.max_retries == 6 + assert copied.max_retries == 7 + + # timeout + assert isinstance(client.timeout, httpx.Timeout) + copied = client.copy(timeout=None) + assert copied.timeout is None + assert isinstance(client.timeout, httpx.Timeout) + + def test_copy_default_headers(self) -> None: + client = Stagehand( + base_url=base_url, api_key=api_key, _strict_response_validation=True, default_headers={"X-Foo": "bar"} + ) + assert client.default_headers["X-Foo"] == "bar" + + # does not override the already given value when not specified + copied = client.copy() + assert copied.default_headers["X-Foo"] == "bar" + + # merges already given headers + copied = client.copy(default_headers={"X-Bar": "stainless"}) + assert copied.default_headers["X-Foo"] == "bar" + assert copied.default_headers["X-Bar"] == "stainless" + + # uses new values for any already given headers + copied = client.copy(default_headers={"X-Foo": "stainless"}) + assert copied.default_headers["X-Foo"] == "stainless" + + # set_default_headers + + # completely overrides already set values + copied = client.copy(set_default_headers={}) + assert copied.default_headers.get("X-Foo") is None + + copied = client.copy(set_default_headers={"X-Bar": "Robert"}) + assert copied.default_headers["X-Bar"] == "Robert" + + with pytest.raises( + ValueError, + match="`default_headers` and `set_default_headers` arguments are mutually exclusive", + ): + client.copy(set_default_headers={}, default_headers={"X-Foo": "Bar"}) + client.close() + + def test_copy_default_query(self) -> None: + client = Stagehand( + base_url=base_url, api_key=api_key, _strict_response_validation=True, default_query={"foo": "bar"} + ) + assert _get_params(client)["foo"] == "bar" + + # does not override the already given value when not specified + copied = client.copy() + assert _get_params(copied)["foo"] == "bar" + + # merges already given params + copied = client.copy(default_query={"bar": "stainless"}) + params = _get_params(copied) + assert params["foo"] == "bar" + assert params["bar"] == "stainless" + + # uses new values for any already given headers + copied = client.copy(default_query={"foo": "stainless"}) + assert _get_params(copied)["foo"] == "stainless" + + # set_default_query + + # completely overrides already set values + copied = client.copy(set_default_query={}) + assert _get_params(copied) == {} + + copied = client.copy(set_default_query={"bar": "Robert"}) + assert _get_params(copied)["bar"] == "Robert" + + with pytest.raises( + ValueError, + # TODO: update + match="`default_query` and `set_default_query` arguments are mutually exclusive", + ): + client.copy(set_default_query={}, default_query={"foo": "Bar"}) + + client.close() + + def test_copy_signature(self, client: Stagehand) -> None: + # ensure the same parameters that can be passed to the client are defined in the `.copy()` method + init_signature = inspect.signature( + # mypy doesn't like that we access the `__init__` property. + client.__init__, # type: ignore[misc] + ) + copy_signature = inspect.signature(client.copy) + exclude_params = {"transport", "proxies", "_strict_response_validation"} + + for name in init_signature.parameters.keys(): + if name in exclude_params: + continue + + copy_param = copy_signature.parameters.get(name) + assert copy_param is not None, f"copy() signature is missing the {name} param" + + @pytest.mark.skipif(sys.version_info >= (3, 10), reason="fails because of a memory leak that started from 3.12") + def test_copy_build_request(self, client: Stagehand) -> None: + options = FinalRequestOptions(method="get", url="/foo") + + def build_request(options: FinalRequestOptions) -> None: + client_copy = client.copy() + client_copy._build_request(options) + + # ensure that the machinery is warmed up before tracing starts. + build_request(options) + gc.collect() + + tracemalloc.start(1000) + + snapshot_before = tracemalloc.take_snapshot() + + ITERATIONS = 10 + for _ in range(ITERATIONS): + build_request(options) + + gc.collect() + snapshot_after = tracemalloc.take_snapshot() + + tracemalloc.stop() + + def add_leak(leaks: list[tracemalloc.StatisticDiff], diff: tracemalloc.StatisticDiff) -> None: + if diff.count == 0: + # Avoid false positives by considering only leaks (i.e. allocations that persist). + return + + if diff.count % ITERATIONS != 0: + # Avoid false positives by considering only leaks that appear per iteration. + return + + for frame in diff.traceback: + if any( + frame.filename.endswith(fragment) + for fragment in [ + # to_raw_response_wrapper leaks through the @functools.wraps() decorator. + # + # removing the decorator fixes the leak for reasons we don't understand. + "stagehand/_legacy_response.py", + "stagehand/_response.py", + # pydantic.BaseModel.model_dump || pydantic.BaseModel.dict leak memory for some reason. + "stagehand/_compat.py", + # Standard library leaks we don't care about. + "/logging/__init__.py", + ] + ): + return + + leaks.append(diff) + + leaks: list[tracemalloc.StatisticDiff] = [] + for diff in snapshot_after.compare_to(snapshot_before, "traceback"): + add_leak(leaks, diff) + if leaks: + for leak in leaks: + print("MEMORY LEAK:", leak) + for frame in leak.traceback: + print(frame) + raise AssertionError() + + def test_request_timeout(self, client: Stagehand) -> None: + request = client._build_request(FinalRequestOptions(method="get", url="/foo")) + timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore + assert timeout == DEFAULT_TIMEOUT + + request = client._build_request(FinalRequestOptions(method="get", url="/foo", timeout=httpx.Timeout(100.0))) + timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore + assert timeout == httpx.Timeout(100.0) + + def test_client_timeout_option(self) -> None: + client = Stagehand( + base_url=base_url, api_key=api_key, _strict_response_validation=True, timeout=httpx.Timeout(0) + ) + + request = client._build_request(FinalRequestOptions(method="get", url="/foo")) + timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore + assert timeout == httpx.Timeout(0) + + client.close() + + def test_http_client_timeout_option(self) -> None: + # custom timeout given to the httpx client should be used + with httpx.Client(timeout=None) as http_client: + client = Stagehand( + base_url=base_url, api_key=api_key, _strict_response_validation=True, http_client=http_client + ) + + request = client._build_request(FinalRequestOptions(method="get", url="/foo")) + timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore + assert timeout == httpx.Timeout(None) + + client.close() + + # no timeout given to the httpx client should not use the httpx default + with httpx.Client() as http_client: + client = Stagehand( + base_url=base_url, api_key=api_key, _strict_response_validation=True, http_client=http_client + ) + + request = client._build_request(FinalRequestOptions(method="get", url="/foo")) + timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore + assert timeout == DEFAULT_TIMEOUT + + client.close() + + # explicitly passing the default timeout currently results in it being ignored + with httpx.Client(timeout=HTTPX_DEFAULT_TIMEOUT) as http_client: + client = Stagehand( + base_url=base_url, api_key=api_key, _strict_response_validation=True, http_client=http_client + ) + + request = client._build_request(FinalRequestOptions(method="get", url="/foo")) + timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore + assert timeout == DEFAULT_TIMEOUT # our default + + client.close() + + async def test_invalid_http_client(self) -> None: + with pytest.raises(TypeError, match="Invalid `http_client` arg"): + async with httpx.AsyncClient() as http_client: + Stagehand( + base_url=base_url, + api_key=api_key, + _strict_response_validation=True, + http_client=cast(Any, http_client), + ) + + def test_default_headers_option(self) -> None: + test_client = Stagehand( + base_url=base_url, api_key=api_key, _strict_response_validation=True, default_headers={"X-Foo": "bar"} + ) + request = test_client._build_request(FinalRequestOptions(method="get", url="/foo")) + assert request.headers.get("x-foo") == "bar" + assert request.headers.get("x-stainless-lang") == "python" + + test_client2 = Stagehand( + base_url=base_url, + api_key=api_key, + _strict_response_validation=True, + default_headers={ + "X-Foo": "stainless", + "X-Stainless-Lang": "my-overriding-header", + }, + ) + request = test_client2._build_request(FinalRequestOptions(method="get", url="/foo")) + assert request.headers.get("x-foo") == "stainless" + assert request.headers.get("x-stainless-lang") == "my-overriding-header" + + test_client.close() + test_client2.close() + + def test_validate_headers(self) -> None: + client = Stagehand(base_url=base_url, api_key=api_key, _strict_response_validation=True) + request = client._build_request(FinalRequestOptions(method="get", url="/foo")) + assert request.headers.get("Authorization") == f"Bearer {api_key}" + + with update_env(**{"STAGEHAND_API_KEY": Omit()}): + client2 = Stagehand(base_url=base_url, api_key=None, _strict_response_validation=True) + + with pytest.raises( + TypeError, + match="Could not resolve authentication method. Expected the api_key to be set. Or for the `Authorization` headers to be explicitly omitted", + ): + client2._build_request(FinalRequestOptions(method="get", url="/foo")) + + request2 = client2._build_request( + FinalRequestOptions(method="get", url="/foo", headers={"Authorization": Omit()}) + ) + assert request2.headers.get("Authorization") is None + + def test_default_query_option(self) -> None: + client = Stagehand( + base_url=base_url, api_key=api_key, _strict_response_validation=True, default_query={"query_param": "bar"} + ) + request = client._build_request(FinalRequestOptions(method="get", url="/foo")) + url = httpx.URL(request.url) + assert dict(url.params) == {"query_param": "bar"} + + request = client._build_request( + FinalRequestOptions( + method="get", + url="/foo", + params={"foo": "baz", "query_param": "overridden"}, + ) + ) + url = httpx.URL(request.url) + assert dict(url.params) == {"foo": "baz", "query_param": "overridden"} + + client.close() + + def test_request_extra_json(self, client: Stagehand) -> None: + request = client._build_request( + FinalRequestOptions( + method="post", + url="/foo", + json_data={"foo": "bar"}, + extra_json={"baz": False}, + ), + ) + data = json.loads(request.content.decode("utf-8")) + assert data == {"foo": "bar", "baz": False} + + request = client._build_request( + FinalRequestOptions( + method="post", + url="/foo", + extra_json={"baz": False}, + ), + ) + data = json.loads(request.content.decode("utf-8")) + assert data == {"baz": False} + + # `extra_json` takes priority over `json_data` when keys clash + request = client._build_request( + FinalRequestOptions( + method="post", + url="/foo", + json_data={"foo": "bar", "baz": True}, + extra_json={"baz": None}, + ), + ) + data = json.loads(request.content.decode("utf-8")) + assert data == {"foo": "bar", "baz": None} + + def test_request_extra_headers(self, client: Stagehand) -> None: + request = client._build_request( + FinalRequestOptions( + method="post", + url="/foo", + **make_request_options(extra_headers={"X-Foo": "Foo"}), + ), + ) + assert request.headers.get("X-Foo") == "Foo" + + # `extra_headers` takes priority over `default_headers` when keys clash + request = client.with_options(default_headers={"X-Bar": "true"})._build_request( + FinalRequestOptions( + method="post", + url="/foo", + **make_request_options( + extra_headers={"X-Bar": "false"}, + ), + ), + ) + assert request.headers.get("X-Bar") == "false" + + def test_request_extra_query(self, client: Stagehand) -> None: + request = client._build_request( + FinalRequestOptions( + method="post", + url="/foo", + **make_request_options( + extra_query={"my_query_param": "Foo"}, + ), + ), + ) + params = dict(request.url.params) + assert params == {"my_query_param": "Foo"} + + # if both `query` and `extra_query` are given, they are merged + request = client._build_request( + FinalRequestOptions( + method="post", + url="/foo", + **make_request_options( + query={"bar": "1"}, + extra_query={"foo": "2"}, + ), + ), + ) + params = dict(request.url.params) + assert params == {"bar": "1", "foo": "2"} + + # `extra_query` takes priority over `query` when keys clash + request = client._build_request( + FinalRequestOptions( + method="post", + url="/foo", + **make_request_options( + query={"foo": "1"}, + extra_query={"foo": "2"}, + ), + ), + ) + params = dict(request.url.params) + assert params == {"foo": "2"} + + def test_multipart_repeating_array(self, client: Stagehand) -> None: + request = client._build_request( + FinalRequestOptions.construct( + method="post", + url="/foo", + headers={"Content-Type": "multipart/form-data; boundary=6b7ba517decee4a450543ea6ae821c82"}, + json_data={"array": ["foo", "bar"]}, + files=[("foo.txt", b"hello world")], + ) + ) + + assert request.read().split(b"\r\n") == [ + b"--6b7ba517decee4a450543ea6ae821c82", + b'Content-Disposition: form-data; name="array[]"', + b"", + b"foo", + b"--6b7ba517decee4a450543ea6ae821c82", + b'Content-Disposition: form-data; name="array[]"', + b"", + b"bar", + b"--6b7ba517decee4a450543ea6ae821c82", + b'Content-Disposition: form-data; name="foo.txt"; filename="upload"', + b"Content-Type: application/octet-stream", + b"", + b"hello world", + b"--6b7ba517decee4a450543ea6ae821c82--", + b"", + ] + + @pytest.mark.respx(base_url=base_url) + def test_basic_union_response(self, respx_mock: MockRouter, client: Stagehand) -> None: + class Model1(BaseModel): + name: str + + class Model2(BaseModel): + foo: str + + respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"})) + + response = client.get("/foo", cast_to=cast(Any, Union[Model1, Model2])) + assert isinstance(response, Model2) + assert response.foo == "bar" + + @pytest.mark.respx(base_url=base_url) + def test_union_response_different_types(self, respx_mock: MockRouter, client: Stagehand) -> None: + """Union of objects with the same field name using a different type""" + + class Model1(BaseModel): + foo: int + + class Model2(BaseModel): + foo: str + + respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"})) + + response = client.get("/foo", cast_to=cast(Any, Union[Model1, Model2])) + assert isinstance(response, Model2) + assert response.foo == "bar" + + respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": 1})) + + response = client.get("/foo", cast_to=cast(Any, Union[Model1, Model2])) + assert isinstance(response, Model1) + assert response.foo == 1 + + @pytest.mark.respx(base_url=base_url) + def test_non_application_json_content_type_for_json_data(self, respx_mock: MockRouter, client: Stagehand) -> None: + """ + Response that sets Content-Type to something other than application/json but returns json data + """ + + class Model(BaseModel): + foo: int + + respx_mock.get("/foo").mock( + return_value=httpx.Response( + 200, + content=json.dumps({"foo": 2}), + headers={"Content-Type": "application/text"}, + ) + ) + + response = client.get("/foo", cast_to=Model) + assert isinstance(response, Model) + assert response.foo == 2 + + def test_base_url_setter(self) -> None: + client = Stagehand(base_url="https://example.com/from_init", api_key=api_key, _strict_response_validation=True) + assert client.base_url == "https://example.com/from_init/" + + client.base_url = "https://example.com/from_setter" # type: ignore[assignment] + + assert client.base_url == "https://example.com/from_setter/" + + client.close() + + def test_base_url_env(self) -> None: + with update_env(STAGEHAND_BASE_URL="http://localhost:5000/from/env"): + client = Stagehand(api_key=api_key, _strict_response_validation=True) + assert client.base_url == "http://localhost:5000/from/env/" + + # explicit environment arg requires explicitness + with update_env(STAGEHAND_BASE_URL="http://localhost:5000/from/env"): + with pytest.raises(ValueError, match=r"you must pass base_url=None"): + Stagehand(api_key=api_key, _strict_response_validation=True, environment="production") + + client = Stagehand( + base_url=None, api_key=api_key, _strict_response_validation=True, environment="production" + ) + assert str(client.base_url).startswith("http://localhost:3000/v1") + + client.close() + + @pytest.mark.parametrize( + "client", + [ + Stagehand(base_url="http://localhost:5000/custom/path/", api_key=api_key, _strict_response_validation=True), + Stagehand( + base_url="http://localhost:5000/custom/path/", + api_key=api_key, + _strict_response_validation=True, + http_client=httpx.Client(), + ), + ], + ids=["standard", "custom http client"], + ) + def test_base_url_trailing_slash(self, client: Stagehand) -> None: + request = client._build_request( + FinalRequestOptions( + method="post", + url="/foo", + json_data={"foo": "bar"}, + ), + ) + assert request.url == "http://localhost:5000/custom/path/foo" + client.close() + + @pytest.mark.parametrize( + "client", + [ + Stagehand(base_url="http://localhost:5000/custom/path/", api_key=api_key, _strict_response_validation=True), + Stagehand( + base_url="http://localhost:5000/custom/path/", + api_key=api_key, + _strict_response_validation=True, + http_client=httpx.Client(), + ), + ], + ids=["standard", "custom http client"], + ) + def test_base_url_no_trailing_slash(self, client: Stagehand) -> None: + request = client._build_request( + FinalRequestOptions( + method="post", + url="/foo", + json_data={"foo": "bar"}, + ), + ) + assert request.url == "http://localhost:5000/custom/path/foo" + client.close() + + @pytest.mark.parametrize( + "client", + [ + Stagehand(base_url="http://localhost:5000/custom/path/", api_key=api_key, _strict_response_validation=True), + Stagehand( + base_url="http://localhost:5000/custom/path/", + api_key=api_key, + _strict_response_validation=True, + http_client=httpx.Client(), + ), + ], + ids=["standard", "custom http client"], + ) + def test_absolute_request_url(self, client: Stagehand) -> None: + request = client._build_request( + FinalRequestOptions( + method="post", + url="https://myapi.com/foo", + json_data={"foo": "bar"}, + ), + ) + assert request.url == "https://myapi.com/foo" + client.close() + + def test_copied_client_does_not_close_http(self) -> None: + test_client = Stagehand(base_url=base_url, api_key=api_key, _strict_response_validation=True) + assert not test_client.is_closed() + + copied = test_client.copy() + assert copied is not test_client + + del copied + + assert not test_client.is_closed() + + def test_client_context_manager(self) -> None: + test_client = Stagehand(base_url=base_url, api_key=api_key, _strict_response_validation=True) + with test_client as c2: + assert c2 is test_client + assert not c2.is_closed() + assert not test_client.is_closed() + assert test_client.is_closed() + + @pytest.mark.respx(base_url=base_url) + def test_client_response_validation_error(self, respx_mock: MockRouter, client: Stagehand) -> None: + class Model(BaseModel): + foo: str + + respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": {"invalid": True}})) + + with pytest.raises(APIResponseValidationError) as exc: + client.get("/foo", cast_to=Model) + + assert isinstance(exc.value.__cause__, ValidationError) + + def test_client_max_retries_validation(self) -> None: + with pytest.raises(TypeError, match=r"max_retries cannot be None"): + Stagehand(base_url=base_url, api_key=api_key, _strict_response_validation=True, max_retries=cast(Any, None)) + + @pytest.mark.respx(base_url=base_url) + def test_received_text_for_expected_json(self, respx_mock: MockRouter) -> None: + class Model(BaseModel): + name: str + + respx_mock.get("/foo").mock(return_value=httpx.Response(200, text="my-custom-format")) + + strict_client = Stagehand(base_url=base_url, api_key=api_key, _strict_response_validation=True) + + with pytest.raises(APIResponseValidationError): + strict_client.get("/foo", cast_to=Model) + + non_strict_client = Stagehand(base_url=base_url, api_key=api_key, _strict_response_validation=False) + + response = non_strict_client.get("/foo", cast_to=Model) + assert isinstance(response, str) # type: ignore[unreachable] + + strict_client.close() + non_strict_client.close() + + @pytest.mark.parametrize( + "remaining_retries,retry_after,timeout", + [ + [3, "20", 20], + [3, "0", 0.5], + [3, "-10", 0.5], + [3, "60", 60], + [3, "61", 0.5], + [3, "Fri, 29 Sep 2023 16:26:57 GMT", 20], + [3, "Fri, 29 Sep 2023 16:26:37 GMT", 0.5], + [3, "Fri, 29 Sep 2023 16:26:27 GMT", 0.5], + [3, "Fri, 29 Sep 2023 16:27:37 GMT", 60], + [3, "Fri, 29 Sep 2023 16:27:38 GMT", 0.5], + [3, "99999999999999999999999999999999999", 0.5], + [3, "Zun, 29 Sep 2023 16:26:27 GMT", 0.5], + [3, "", 0.5], + [2, "", 0.5 * 2.0], + [1, "", 0.5 * 4.0], + [-1100, "", 8], # test large number potentially overflowing + ], + ) + @mock.patch("time.time", mock.MagicMock(return_value=1696004797)) + def test_parse_retry_after_header( + self, remaining_retries: int, retry_after: str, timeout: float, client: Stagehand + ) -> None: + headers = httpx.Headers({"retry-after": retry_after}) + options = FinalRequestOptions(method="get", url="/foo", max_retries=3) + calculated = client._calculate_retry_timeout(remaining_retries, options, headers) + assert calculated == pytest.approx(timeout, 0.5 * 0.875) # pyright: ignore[reportUnknownMemberType] + + @mock.patch("stagehand._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) + @pytest.mark.respx(base_url=base_url) + def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter, client: Stagehand) -> None: + respx_mock.post("/sessions/start").mock(side_effect=httpx.TimeoutException("Test timeout error")) + + with pytest.raises(APITimeoutError): + client.sessions.with_streaming_response.start(env="LOCAL").__enter__() + + assert _get_open_connections(client) == 0 + + @mock.patch("stagehand._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) + @pytest.mark.respx(base_url=base_url) + def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter, client: Stagehand) -> None: + respx_mock.post("/sessions/start").mock(return_value=httpx.Response(500)) + + with pytest.raises(APIStatusError): + client.sessions.with_streaming_response.start(env="LOCAL").__enter__() + assert _get_open_connections(client) == 0 + + @pytest.mark.parametrize("failures_before_success", [0, 2, 4]) + @mock.patch("stagehand._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) + @pytest.mark.respx(base_url=base_url) + @pytest.mark.parametrize("failure_mode", ["status", "exception"]) + def test_retries_taken( + self, + client: Stagehand, + failures_before_success: int, + failure_mode: Literal["status", "exception"], + respx_mock: MockRouter, + ) -> None: + client = client.with_options(max_retries=4) + + nb_retries = 0 + + def retry_handler(_request: httpx.Request) -> httpx.Response: + nonlocal nb_retries + if nb_retries < failures_before_success: + nb_retries += 1 + if failure_mode == "exception": + raise RuntimeError("oops") + return httpx.Response(500) + return httpx.Response(200) + + respx_mock.post("/sessions/start").mock(side_effect=retry_handler) + + response = client.sessions.with_raw_response.start(env="LOCAL") + + assert response.retries_taken == failures_before_success + assert int(response.http_request.headers.get("x-stainless-retry-count")) == failures_before_success + + @pytest.mark.parametrize("failures_before_success", [0, 2, 4]) + @mock.patch("stagehand._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) + @pytest.mark.respx(base_url=base_url) + def test_omit_retry_count_header( + self, client: Stagehand, failures_before_success: int, respx_mock: MockRouter + ) -> None: + client = client.with_options(max_retries=4) + + nb_retries = 0 + + def retry_handler(_request: httpx.Request) -> httpx.Response: + nonlocal nb_retries + if nb_retries < failures_before_success: + nb_retries += 1 + return httpx.Response(500) + return httpx.Response(200) + + respx_mock.post("/sessions/start").mock(side_effect=retry_handler) + + response = client.sessions.with_raw_response.start( + env="LOCAL", extra_headers={"x-stainless-retry-count": Omit()} + ) + + assert len(response.http_request.headers.get_list("x-stainless-retry-count")) == 0 + + @pytest.mark.parametrize("failures_before_success", [0, 2, 4]) + @mock.patch("stagehand._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) + @pytest.mark.respx(base_url=base_url) + def test_overwrite_retry_count_header( + self, client: Stagehand, failures_before_success: int, respx_mock: MockRouter + ) -> None: + client = client.with_options(max_retries=4) + + nb_retries = 0 + + def retry_handler(_request: httpx.Request) -> httpx.Response: + nonlocal nb_retries + if nb_retries < failures_before_success: + nb_retries += 1 + return httpx.Response(500) + return httpx.Response(200) + + respx_mock.post("/sessions/start").mock(side_effect=retry_handler) + + response = client.sessions.with_raw_response.start(env="LOCAL", extra_headers={"x-stainless-retry-count": "42"}) + + assert response.http_request.headers.get("x-stainless-retry-count") == "42" + + def test_proxy_environment_variables(self, monkeypatch: pytest.MonkeyPatch) -> None: + # Test that the proxy environment variables are set correctly + monkeypatch.setenv("HTTPS_PROXY", "https://example.org") + + client = DefaultHttpxClient() + + mounts = tuple(client._mounts.items()) + assert len(mounts) == 1 + assert mounts[0][0].pattern == "https://" + + @pytest.mark.filterwarnings("ignore:.*deprecated.*:DeprecationWarning") + def test_default_client_creation(self) -> None: + # Ensure that the client can be initialized without any exceptions + DefaultHttpxClient( + verify=True, + cert=None, + trust_env=True, + http1=True, + http2=False, + limits=httpx.Limits(max_connections=100, max_keepalive_connections=20), + ) + + @pytest.mark.respx(base_url=base_url) + def test_follow_redirects(self, respx_mock: MockRouter, client: Stagehand) -> None: + # Test that the default follow_redirects=True allows following redirects + respx_mock.post("/redirect").mock( + return_value=httpx.Response(302, headers={"Location": f"{base_url}/redirected"}) + ) + respx_mock.get("/redirected").mock(return_value=httpx.Response(200, json={"status": "ok"})) + + response = client.post("/redirect", body={"key": "value"}, cast_to=httpx.Response) + assert response.status_code == 200 + assert response.json() == {"status": "ok"} + + @pytest.mark.respx(base_url=base_url) + def test_follow_redirects_disabled(self, respx_mock: MockRouter, client: Stagehand) -> None: + # Test that follow_redirects=False prevents following redirects + respx_mock.post("/redirect").mock( + return_value=httpx.Response(302, headers={"Location": f"{base_url}/redirected"}) + ) + + with pytest.raises(APIStatusError) as exc_info: + client.post("/redirect", body={"key": "value"}, options={"follow_redirects": False}, cast_to=httpx.Response) + + assert exc_info.value.response.status_code == 302 + assert exc_info.value.response.headers["Location"] == f"{base_url}/redirected" + + +class TestAsyncStagehand: + @pytest.mark.respx(base_url=base_url) + async def test_raw_response(self, respx_mock: MockRouter, async_client: AsyncStagehand) -> None: + respx_mock.post("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"})) + + response = await async_client.post("/foo", cast_to=httpx.Response) + assert response.status_code == 200 + assert isinstance(response, httpx.Response) + assert response.json() == {"foo": "bar"} + + @pytest.mark.respx(base_url=base_url) + async def test_raw_response_for_binary(self, respx_mock: MockRouter, async_client: AsyncStagehand) -> None: + respx_mock.post("/foo").mock( + return_value=httpx.Response(200, headers={"Content-Type": "application/binary"}, content='{"foo": "bar"}') + ) + + response = await async_client.post("/foo", cast_to=httpx.Response) + assert response.status_code == 200 + assert isinstance(response, httpx.Response) + assert response.json() == {"foo": "bar"} + + def test_copy(self, async_client: AsyncStagehand) -> None: + copied = async_client.copy() + assert id(copied) != id(async_client) + + copied = async_client.copy(api_key="another My API Key") + assert copied.api_key == "another My API Key" + assert async_client.api_key == "My API Key" + + def test_copy_default_options(self, async_client: AsyncStagehand) -> None: + # options that have a default are overridden correctly + copied = async_client.copy(max_retries=7) + assert copied.max_retries == 7 + assert async_client.max_retries == 2 + + copied2 = copied.copy(max_retries=6) + assert copied2.max_retries == 6 + assert copied.max_retries == 7 + + # timeout + assert isinstance(async_client.timeout, httpx.Timeout) + copied = async_client.copy(timeout=None) + assert copied.timeout is None + assert isinstance(async_client.timeout, httpx.Timeout) + + async def test_copy_default_headers(self) -> None: + client = AsyncStagehand( + base_url=base_url, api_key=api_key, _strict_response_validation=True, default_headers={"X-Foo": "bar"} + ) + assert client.default_headers["X-Foo"] == "bar" + + # does not override the already given value when not specified + copied = client.copy() + assert copied.default_headers["X-Foo"] == "bar" + + # merges already given headers + copied = client.copy(default_headers={"X-Bar": "stainless"}) + assert copied.default_headers["X-Foo"] == "bar" + assert copied.default_headers["X-Bar"] == "stainless" + + # uses new values for any already given headers + copied = client.copy(default_headers={"X-Foo": "stainless"}) + assert copied.default_headers["X-Foo"] == "stainless" + + # set_default_headers + + # completely overrides already set values + copied = client.copy(set_default_headers={}) + assert copied.default_headers.get("X-Foo") is None + + copied = client.copy(set_default_headers={"X-Bar": "Robert"}) + assert copied.default_headers["X-Bar"] == "Robert" + + with pytest.raises( + ValueError, + match="`default_headers` and `set_default_headers` arguments are mutually exclusive", + ): + client.copy(set_default_headers={}, default_headers={"X-Foo": "Bar"}) + await client.close() + + async def test_copy_default_query(self) -> None: + client = AsyncStagehand( + base_url=base_url, api_key=api_key, _strict_response_validation=True, default_query={"foo": "bar"} + ) + assert _get_params(client)["foo"] == "bar" + + # does not override the already given value when not specified + copied = client.copy() + assert _get_params(copied)["foo"] == "bar" + + # merges already given params + copied = client.copy(default_query={"bar": "stainless"}) + params = _get_params(copied) + assert params["foo"] == "bar" + assert params["bar"] == "stainless" + + # uses new values for any already given headers + copied = client.copy(default_query={"foo": "stainless"}) + assert _get_params(copied)["foo"] == "stainless" + + # set_default_query + + # completely overrides already set values + copied = client.copy(set_default_query={}) + assert _get_params(copied) == {} + + copied = client.copy(set_default_query={"bar": "Robert"}) + assert _get_params(copied)["bar"] == "Robert" + + with pytest.raises( + ValueError, + # TODO: update + match="`default_query` and `set_default_query` arguments are mutually exclusive", + ): + client.copy(set_default_query={}, default_query={"foo": "Bar"}) + + await client.close() + + def test_copy_signature(self, async_client: AsyncStagehand) -> None: + # ensure the same parameters that can be passed to the client are defined in the `.copy()` method + init_signature = inspect.signature( + # mypy doesn't like that we access the `__init__` property. + async_client.__init__, # type: ignore[misc] + ) + copy_signature = inspect.signature(async_client.copy) + exclude_params = {"transport", "proxies", "_strict_response_validation"} + + for name in init_signature.parameters.keys(): + if name in exclude_params: + continue + + copy_param = copy_signature.parameters.get(name) + assert copy_param is not None, f"copy() signature is missing the {name} param" + + @pytest.mark.skipif(sys.version_info >= (3, 10), reason="fails because of a memory leak that started from 3.12") + def test_copy_build_request(self, async_client: AsyncStagehand) -> None: + options = FinalRequestOptions(method="get", url="/foo") + + def build_request(options: FinalRequestOptions) -> None: + client_copy = async_client.copy() + client_copy._build_request(options) + + # ensure that the machinery is warmed up before tracing starts. + build_request(options) + gc.collect() + + tracemalloc.start(1000) + + snapshot_before = tracemalloc.take_snapshot() + + ITERATIONS = 10 + for _ in range(ITERATIONS): + build_request(options) + + gc.collect() + snapshot_after = tracemalloc.take_snapshot() + + tracemalloc.stop() + + def add_leak(leaks: list[tracemalloc.StatisticDiff], diff: tracemalloc.StatisticDiff) -> None: + if diff.count == 0: + # Avoid false positives by considering only leaks (i.e. allocations that persist). + return + + if diff.count % ITERATIONS != 0: + # Avoid false positives by considering only leaks that appear per iteration. + return + + for frame in diff.traceback: + if any( + frame.filename.endswith(fragment) + for fragment in [ + # to_raw_response_wrapper leaks through the @functools.wraps() decorator. + # + # removing the decorator fixes the leak for reasons we don't understand. + "stagehand/_legacy_response.py", + "stagehand/_response.py", + # pydantic.BaseModel.model_dump || pydantic.BaseModel.dict leak memory for some reason. + "stagehand/_compat.py", + # Standard library leaks we don't care about. + "/logging/__init__.py", + ] + ): + return + + leaks.append(diff) + + leaks: list[tracemalloc.StatisticDiff] = [] + for diff in snapshot_after.compare_to(snapshot_before, "traceback"): + add_leak(leaks, diff) + if leaks: + for leak in leaks: + print("MEMORY LEAK:", leak) + for frame in leak.traceback: + print(frame) + raise AssertionError() + + async def test_request_timeout(self, async_client: AsyncStagehand) -> None: + request = async_client._build_request(FinalRequestOptions(method="get", url="/foo")) + timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore + assert timeout == DEFAULT_TIMEOUT + + request = async_client._build_request( + FinalRequestOptions(method="get", url="/foo", timeout=httpx.Timeout(100.0)) + ) + timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore + assert timeout == httpx.Timeout(100.0) + + async def test_client_timeout_option(self) -> None: + client = AsyncStagehand( + base_url=base_url, api_key=api_key, _strict_response_validation=True, timeout=httpx.Timeout(0) + ) + + request = client._build_request(FinalRequestOptions(method="get", url="/foo")) + timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore + assert timeout == httpx.Timeout(0) + + await client.close() + + async def test_http_client_timeout_option(self) -> None: + # custom timeout given to the httpx client should be used + async with httpx.AsyncClient(timeout=None) as http_client: + client = AsyncStagehand( + base_url=base_url, api_key=api_key, _strict_response_validation=True, http_client=http_client + ) + + request = client._build_request(FinalRequestOptions(method="get", url="/foo")) + timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore + assert timeout == httpx.Timeout(None) + + await client.close() + + # no timeout given to the httpx client should not use the httpx default + async with httpx.AsyncClient() as http_client: + client = AsyncStagehand( + base_url=base_url, api_key=api_key, _strict_response_validation=True, http_client=http_client + ) + + request = client._build_request(FinalRequestOptions(method="get", url="/foo")) + timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore + assert timeout == DEFAULT_TIMEOUT + + await client.close() + + # explicitly passing the default timeout currently results in it being ignored + async with httpx.AsyncClient(timeout=HTTPX_DEFAULT_TIMEOUT) as http_client: + client = AsyncStagehand( + base_url=base_url, api_key=api_key, _strict_response_validation=True, http_client=http_client + ) + + request = client._build_request(FinalRequestOptions(method="get", url="/foo")) + timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore + assert timeout == DEFAULT_TIMEOUT # our default + + await client.close() + + def test_invalid_http_client(self) -> None: + with pytest.raises(TypeError, match="Invalid `http_client` arg"): + with httpx.Client() as http_client: + AsyncStagehand( + base_url=base_url, + api_key=api_key, + _strict_response_validation=True, + http_client=cast(Any, http_client), + ) + + async def test_default_headers_option(self) -> None: + test_client = AsyncStagehand( + base_url=base_url, api_key=api_key, _strict_response_validation=True, default_headers={"X-Foo": "bar"} + ) + request = test_client._build_request(FinalRequestOptions(method="get", url="/foo")) + assert request.headers.get("x-foo") == "bar" + assert request.headers.get("x-stainless-lang") == "python" + + test_client2 = AsyncStagehand( + base_url=base_url, + api_key=api_key, + _strict_response_validation=True, + default_headers={ + "X-Foo": "stainless", + "X-Stainless-Lang": "my-overriding-header", + }, + ) + request = test_client2._build_request(FinalRequestOptions(method="get", url="/foo")) + assert request.headers.get("x-foo") == "stainless" + assert request.headers.get("x-stainless-lang") == "my-overriding-header" + + await test_client.close() + await test_client2.close() + + def test_validate_headers(self) -> None: + client = AsyncStagehand(base_url=base_url, api_key=api_key, _strict_response_validation=True) + request = client._build_request(FinalRequestOptions(method="get", url="/foo")) + assert request.headers.get("Authorization") == f"Bearer {api_key}" + + with update_env(**{"STAGEHAND_API_KEY": Omit()}): + client2 = AsyncStagehand(base_url=base_url, api_key=None, _strict_response_validation=True) + + with pytest.raises( + TypeError, + match="Could not resolve authentication method. Expected the api_key to be set. Or for the `Authorization` headers to be explicitly omitted", + ): + client2._build_request(FinalRequestOptions(method="get", url="/foo")) + + request2 = client2._build_request( + FinalRequestOptions(method="get", url="/foo", headers={"Authorization": Omit()}) + ) + assert request2.headers.get("Authorization") is None + + async def test_default_query_option(self) -> None: + client = AsyncStagehand( + base_url=base_url, api_key=api_key, _strict_response_validation=True, default_query={"query_param": "bar"} + ) + request = client._build_request(FinalRequestOptions(method="get", url="/foo")) + url = httpx.URL(request.url) + assert dict(url.params) == {"query_param": "bar"} + + request = client._build_request( + FinalRequestOptions( + method="get", + url="/foo", + params={"foo": "baz", "query_param": "overridden"}, + ) + ) + url = httpx.URL(request.url) + assert dict(url.params) == {"foo": "baz", "query_param": "overridden"} + + await client.close() + + def test_request_extra_json(self, client: Stagehand) -> None: + request = client._build_request( + FinalRequestOptions( + method="post", + url="/foo", + json_data={"foo": "bar"}, + extra_json={"baz": False}, + ), + ) + data = json.loads(request.content.decode("utf-8")) + assert data == {"foo": "bar", "baz": False} + + request = client._build_request( + FinalRequestOptions( + method="post", + url="/foo", + extra_json={"baz": False}, + ), + ) + data = json.loads(request.content.decode("utf-8")) + assert data == {"baz": False} + + # `extra_json` takes priority over `json_data` when keys clash + request = client._build_request( + FinalRequestOptions( + method="post", + url="/foo", + json_data={"foo": "bar", "baz": True}, + extra_json={"baz": None}, + ), + ) + data = json.loads(request.content.decode("utf-8")) + assert data == {"foo": "bar", "baz": None} + + def test_request_extra_headers(self, client: Stagehand) -> None: + request = client._build_request( + FinalRequestOptions( + method="post", + url="/foo", + **make_request_options(extra_headers={"X-Foo": "Foo"}), + ), + ) + assert request.headers.get("X-Foo") == "Foo" + + # `extra_headers` takes priority over `default_headers` when keys clash + request = client.with_options(default_headers={"X-Bar": "true"})._build_request( + FinalRequestOptions( + method="post", + url="/foo", + **make_request_options( + extra_headers={"X-Bar": "false"}, + ), + ), + ) + assert request.headers.get("X-Bar") == "false" + + def test_request_extra_query(self, client: Stagehand) -> None: + request = client._build_request( + FinalRequestOptions( + method="post", + url="/foo", + **make_request_options( + extra_query={"my_query_param": "Foo"}, + ), + ), + ) + params = dict(request.url.params) + assert params == {"my_query_param": "Foo"} + + # if both `query` and `extra_query` are given, they are merged + request = client._build_request( + FinalRequestOptions( + method="post", + url="/foo", + **make_request_options( + query={"bar": "1"}, + extra_query={"foo": "2"}, + ), + ), + ) + params = dict(request.url.params) + assert params == {"bar": "1", "foo": "2"} + + # `extra_query` takes priority over `query` when keys clash + request = client._build_request( + FinalRequestOptions( + method="post", + url="/foo", + **make_request_options( + query={"foo": "1"}, + extra_query={"foo": "2"}, + ), + ), + ) + params = dict(request.url.params) + assert params == {"foo": "2"} + + def test_multipart_repeating_array(self, async_client: AsyncStagehand) -> None: + request = async_client._build_request( + FinalRequestOptions.construct( + method="post", + url="/foo", + headers={"Content-Type": "multipart/form-data; boundary=6b7ba517decee4a450543ea6ae821c82"}, + json_data={"array": ["foo", "bar"]}, + files=[("foo.txt", b"hello world")], + ) + ) + + assert request.read().split(b"\r\n") == [ + b"--6b7ba517decee4a450543ea6ae821c82", + b'Content-Disposition: form-data; name="array[]"', + b"", + b"foo", + b"--6b7ba517decee4a450543ea6ae821c82", + b'Content-Disposition: form-data; name="array[]"', + b"", + b"bar", + b"--6b7ba517decee4a450543ea6ae821c82", + b'Content-Disposition: form-data; name="foo.txt"; filename="upload"', + b"Content-Type: application/octet-stream", + b"", + b"hello world", + b"--6b7ba517decee4a450543ea6ae821c82--", + b"", + ] + + @pytest.mark.respx(base_url=base_url) + async def test_basic_union_response(self, respx_mock: MockRouter, async_client: AsyncStagehand) -> None: + class Model1(BaseModel): + name: str + + class Model2(BaseModel): + foo: str + + respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"})) + + response = await async_client.get("/foo", cast_to=cast(Any, Union[Model1, Model2])) + assert isinstance(response, Model2) + assert response.foo == "bar" + + @pytest.mark.respx(base_url=base_url) + async def test_union_response_different_types(self, respx_mock: MockRouter, async_client: AsyncStagehand) -> None: + """Union of objects with the same field name using a different type""" + + class Model1(BaseModel): + foo: int + + class Model2(BaseModel): + foo: str + + respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"})) + + response = await async_client.get("/foo", cast_to=cast(Any, Union[Model1, Model2])) + assert isinstance(response, Model2) + assert response.foo == "bar" + + respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": 1})) + + response = await async_client.get("/foo", cast_to=cast(Any, Union[Model1, Model2])) + assert isinstance(response, Model1) + assert response.foo == 1 + + @pytest.mark.respx(base_url=base_url) + async def test_non_application_json_content_type_for_json_data( + self, respx_mock: MockRouter, async_client: AsyncStagehand + ) -> None: + """ + Response that sets Content-Type to something other than application/json but returns json data + """ + + class Model(BaseModel): + foo: int + + respx_mock.get("/foo").mock( + return_value=httpx.Response( + 200, + content=json.dumps({"foo": 2}), + headers={"Content-Type": "application/text"}, + ) + ) + + response = await async_client.get("/foo", cast_to=Model) + assert isinstance(response, Model) + assert response.foo == 2 + + async def test_base_url_setter(self) -> None: + client = AsyncStagehand( + base_url="https://example.com/from_init", api_key=api_key, _strict_response_validation=True + ) + assert client.base_url == "https://example.com/from_init/" + + client.base_url = "https://example.com/from_setter" # type: ignore[assignment] + + assert client.base_url == "https://example.com/from_setter/" + + await client.close() + + async def test_base_url_env(self) -> None: + with update_env(STAGEHAND_BASE_URL="http://localhost:5000/from/env"): + client = AsyncStagehand(api_key=api_key, _strict_response_validation=True) + assert client.base_url == "http://localhost:5000/from/env/" + + # explicit environment arg requires explicitness + with update_env(STAGEHAND_BASE_URL="http://localhost:5000/from/env"): + with pytest.raises(ValueError, match=r"you must pass base_url=None"): + AsyncStagehand(api_key=api_key, _strict_response_validation=True, environment="production") + + client = AsyncStagehand( + base_url=None, api_key=api_key, _strict_response_validation=True, environment="production" + ) + assert str(client.base_url).startswith("http://localhost:3000/v1") + + await client.close() + + @pytest.mark.parametrize( + "client", + [ + AsyncStagehand( + base_url="http://localhost:5000/custom/path/", api_key=api_key, _strict_response_validation=True + ), + AsyncStagehand( + base_url="http://localhost:5000/custom/path/", + api_key=api_key, + _strict_response_validation=True, + http_client=httpx.AsyncClient(), + ), + ], + ids=["standard", "custom http client"], + ) + async def test_base_url_trailing_slash(self, client: AsyncStagehand) -> None: + request = client._build_request( + FinalRequestOptions( + method="post", + url="/foo", + json_data={"foo": "bar"}, + ), + ) + assert request.url == "http://localhost:5000/custom/path/foo" + await client.close() + + @pytest.mark.parametrize( + "client", + [ + AsyncStagehand( + base_url="http://localhost:5000/custom/path/", api_key=api_key, _strict_response_validation=True + ), + AsyncStagehand( + base_url="http://localhost:5000/custom/path/", + api_key=api_key, + _strict_response_validation=True, + http_client=httpx.AsyncClient(), + ), + ], + ids=["standard", "custom http client"], + ) + async def test_base_url_no_trailing_slash(self, client: AsyncStagehand) -> None: + request = client._build_request( + FinalRequestOptions( + method="post", + url="/foo", + json_data={"foo": "bar"}, + ), + ) + assert request.url == "http://localhost:5000/custom/path/foo" + await client.close() + + @pytest.mark.parametrize( + "client", + [ + AsyncStagehand( + base_url="http://localhost:5000/custom/path/", api_key=api_key, _strict_response_validation=True + ), + AsyncStagehand( + base_url="http://localhost:5000/custom/path/", + api_key=api_key, + _strict_response_validation=True, + http_client=httpx.AsyncClient(), + ), + ], + ids=["standard", "custom http client"], + ) + async def test_absolute_request_url(self, client: AsyncStagehand) -> None: + request = client._build_request( + FinalRequestOptions( + method="post", + url="https://myapi.com/foo", + json_data={"foo": "bar"}, + ), + ) + assert request.url == "https://myapi.com/foo" + await client.close() + + async def test_copied_client_does_not_close_http(self) -> None: + test_client = AsyncStagehand(base_url=base_url, api_key=api_key, _strict_response_validation=True) + assert not test_client.is_closed() + + copied = test_client.copy() + assert copied is not test_client + + del copied + + await asyncio.sleep(0.2) + assert not test_client.is_closed() + + async def test_client_context_manager(self) -> None: + test_client = AsyncStagehand(base_url=base_url, api_key=api_key, _strict_response_validation=True) + async with test_client as c2: + assert c2 is test_client + assert not c2.is_closed() + assert not test_client.is_closed() + assert test_client.is_closed() + + @pytest.mark.respx(base_url=base_url) + async def test_client_response_validation_error(self, respx_mock: MockRouter, async_client: AsyncStagehand) -> None: + class Model(BaseModel): + foo: str + + respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": {"invalid": True}})) + + with pytest.raises(APIResponseValidationError) as exc: + await async_client.get("/foo", cast_to=Model) + + assert isinstance(exc.value.__cause__, ValidationError) + + async def test_client_max_retries_validation(self) -> None: + with pytest.raises(TypeError, match=r"max_retries cannot be None"): + AsyncStagehand( + base_url=base_url, api_key=api_key, _strict_response_validation=True, max_retries=cast(Any, None) + ) + + @pytest.mark.respx(base_url=base_url) + async def test_received_text_for_expected_json(self, respx_mock: MockRouter) -> None: + class Model(BaseModel): + name: str + + respx_mock.get("/foo").mock(return_value=httpx.Response(200, text="my-custom-format")) + + strict_client = AsyncStagehand(base_url=base_url, api_key=api_key, _strict_response_validation=True) + + with pytest.raises(APIResponseValidationError): + await strict_client.get("/foo", cast_to=Model) + + non_strict_client = AsyncStagehand(base_url=base_url, api_key=api_key, _strict_response_validation=False) + + response = await non_strict_client.get("/foo", cast_to=Model) + assert isinstance(response, str) # type: ignore[unreachable] + + await strict_client.close() + await non_strict_client.close() + + @pytest.mark.parametrize( + "remaining_retries,retry_after,timeout", + [ + [3, "20", 20], + [3, "0", 0.5], + [3, "-10", 0.5], + [3, "60", 60], + [3, "61", 0.5], + [3, "Fri, 29 Sep 2023 16:26:57 GMT", 20], + [3, "Fri, 29 Sep 2023 16:26:37 GMT", 0.5], + [3, "Fri, 29 Sep 2023 16:26:27 GMT", 0.5], + [3, "Fri, 29 Sep 2023 16:27:37 GMT", 60], + [3, "Fri, 29 Sep 2023 16:27:38 GMT", 0.5], + [3, "99999999999999999999999999999999999", 0.5], + [3, "Zun, 29 Sep 2023 16:26:27 GMT", 0.5], + [3, "", 0.5], + [2, "", 0.5 * 2.0], + [1, "", 0.5 * 4.0], + [-1100, "", 8], # test large number potentially overflowing + ], + ) + @mock.patch("time.time", mock.MagicMock(return_value=1696004797)) + async def test_parse_retry_after_header( + self, remaining_retries: int, retry_after: str, timeout: float, async_client: AsyncStagehand + ) -> None: + headers = httpx.Headers({"retry-after": retry_after}) + options = FinalRequestOptions(method="get", url="/foo", max_retries=3) + calculated = async_client._calculate_retry_timeout(remaining_retries, options, headers) + assert calculated == pytest.approx(timeout, 0.5 * 0.875) # pyright: ignore[reportUnknownMemberType] + + @mock.patch("stagehand._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) + @pytest.mark.respx(base_url=base_url) + async def test_retrying_timeout_errors_doesnt_leak( + self, respx_mock: MockRouter, async_client: AsyncStagehand + ) -> None: + respx_mock.post("/sessions/start").mock(side_effect=httpx.TimeoutException("Test timeout error")) + + with pytest.raises(APITimeoutError): + await async_client.sessions.with_streaming_response.start(env="LOCAL").__aenter__() + + assert _get_open_connections(async_client) == 0 + + @mock.patch("stagehand._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) + @pytest.mark.respx(base_url=base_url) + async def test_retrying_status_errors_doesnt_leak( + self, respx_mock: MockRouter, async_client: AsyncStagehand + ) -> None: + respx_mock.post("/sessions/start").mock(return_value=httpx.Response(500)) + + with pytest.raises(APIStatusError): + await async_client.sessions.with_streaming_response.start(env="LOCAL").__aenter__() + assert _get_open_connections(async_client) == 0 + + @pytest.mark.parametrize("failures_before_success", [0, 2, 4]) + @mock.patch("stagehand._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) + @pytest.mark.respx(base_url=base_url) + @pytest.mark.parametrize("failure_mode", ["status", "exception"]) + async def test_retries_taken( + self, + async_client: AsyncStagehand, + failures_before_success: int, + failure_mode: Literal["status", "exception"], + respx_mock: MockRouter, + ) -> None: + client = async_client.with_options(max_retries=4) + + nb_retries = 0 + + def retry_handler(_request: httpx.Request) -> httpx.Response: + nonlocal nb_retries + if nb_retries < failures_before_success: + nb_retries += 1 + if failure_mode == "exception": + raise RuntimeError("oops") + return httpx.Response(500) + return httpx.Response(200) + + respx_mock.post("/sessions/start").mock(side_effect=retry_handler) + + response = await client.sessions.with_raw_response.start(env="LOCAL") + + assert response.retries_taken == failures_before_success + assert int(response.http_request.headers.get("x-stainless-retry-count")) == failures_before_success + + @pytest.mark.parametrize("failures_before_success", [0, 2, 4]) + @mock.patch("stagehand._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) + @pytest.mark.respx(base_url=base_url) + async def test_omit_retry_count_header( + self, async_client: AsyncStagehand, failures_before_success: int, respx_mock: MockRouter + ) -> None: + client = async_client.with_options(max_retries=4) + + nb_retries = 0 + + def retry_handler(_request: httpx.Request) -> httpx.Response: + nonlocal nb_retries + if nb_retries < failures_before_success: + nb_retries += 1 + return httpx.Response(500) + return httpx.Response(200) + + respx_mock.post("/sessions/start").mock(side_effect=retry_handler) + + response = await client.sessions.with_raw_response.start( + env="LOCAL", extra_headers={"x-stainless-retry-count": Omit()} + ) + + assert len(response.http_request.headers.get_list("x-stainless-retry-count")) == 0 + + @pytest.mark.parametrize("failures_before_success", [0, 2, 4]) + @mock.patch("stagehand._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) + @pytest.mark.respx(base_url=base_url) + async def test_overwrite_retry_count_header( + self, async_client: AsyncStagehand, failures_before_success: int, respx_mock: MockRouter + ) -> None: + client = async_client.with_options(max_retries=4) + + nb_retries = 0 + + def retry_handler(_request: httpx.Request) -> httpx.Response: + nonlocal nb_retries + if nb_retries < failures_before_success: + nb_retries += 1 + return httpx.Response(500) + return httpx.Response(200) + + respx_mock.post("/sessions/start").mock(side_effect=retry_handler) + + response = await client.sessions.with_raw_response.start( + env="LOCAL", extra_headers={"x-stainless-retry-count": "42"} + ) + + assert response.http_request.headers.get("x-stainless-retry-count") == "42" + + async def test_get_platform(self) -> None: + platform = await asyncify(get_platform)() + assert isinstance(platform, (str, OtherPlatform)) + + async def test_proxy_environment_variables(self, monkeypatch: pytest.MonkeyPatch) -> None: + # Test that the proxy environment variables are set correctly + monkeypatch.setenv("HTTPS_PROXY", "https://example.org") + + client = DefaultAsyncHttpxClient() + + mounts = tuple(client._mounts.items()) + assert len(mounts) == 1 + assert mounts[0][0].pattern == "https://" + + @pytest.mark.filterwarnings("ignore:.*deprecated.*:DeprecationWarning") + async def test_default_client_creation(self) -> None: + # Ensure that the client can be initialized without any exceptions + DefaultAsyncHttpxClient( + verify=True, + cert=None, + trust_env=True, + http1=True, + http2=False, + limits=httpx.Limits(max_connections=100, max_keepalive_connections=20), + ) + + @pytest.mark.respx(base_url=base_url) + async def test_follow_redirects(self, respx_mock: MockRouter, async_client: AsyncStagehand) -> None: + # Test that the default follow_redirects=True allows following redirects + respx_mock.post("/redirect").mock( + return_value=httpx.Response(302, headers={"Location": f"{base_url}/redirected"}) + ) + respx_mock.get("/redirected").mock(return_value=httpx.Response(200, json={"status": "ok"})) + + response = await async_client.post("/redirect", body={"key": "value"}, cast_to=httpx.Response) + assert response.status_code == 200 + assert response.json() == {"status": "ok"} + + @pytest.mark.respx(base_url=base_url) + async def test_follow_redirects_disabled(self, respx_mock: MockRouter, async_client: AsyncStagehand) -> None: + # Test that follow_redirects=False prevents following redirects + respx_mock.post("/redirect").mock( + return_value=httpx.Response(302, headers={"Location": f"{base_url}/redirected"}) + ) + + with pytest.raises(APIStatusError) as exc_info: + await async_client.post( + "/redirect", body={"key": "value"}, options={"follow_redirects": False}, cast_to=httpx.Response + ) + + assert exc_info.value.response.status_code == 302 + assert exc_info.value.response.headers["Location"] == f"{base_url}/redirected" diff --git a/tests/test_deepcopy.py b/tests/test_deepcopy.py new file mode 100644 index 00000000..889b2d4e --- /dev/null +++ b/tests/test_deepcopy.py @@ -0,0 +1,58 @@ +from stagehand._utils import deepcopy_minimal + + +def assert_different_identities(obj1: object, obj2: object) -> None: + assert obj1 == obj2 + assert id(obj1) != id(obj2) + + +def test_simple_dict() -> None: + obj1 = {"foo": "bar"} + obj2 = deepcopy_minimal(obj1) + assert_different_identities(obj1, obj2) + + +def test_nested_dict() -> None: + obj1 = {"foo": {"bar": True}} + obj2 = deepcopy_minimal(obj1) + assert_different_identities(obj1, obj2) + assert_different_identities(obj1["foo"], obj2["foo"]) + + +def test_complex_nested_dict() -> None: + obj1 = {"foo": {"bar": [{"hello": "world"}]}} + obj2 = deepcopy_minimal(obj1) + assert_different_identities(obj1, obj2) + assert_different_identities(obj1["foo"], obj2["foo"]) + assert_different_identities(obj1["foo"]["bar"], obj2["foo"]["bar"]) + assert_different_identities(obj1["foo"]["bar"][0], obj2["foo"]["bar"][0]) + + +def test_simple_list() -> None: + obj1 = ["a", "b", "c"] + obj2 = deepcopy_minimal(obj1) + assert_different_identities(obj1, obj2) + + +def test_nested_list() -> None: + obj1 = ["a", [1, 2, 3]] + obj2 = deepcopy_minimal(obj1) + assert_different_identities(obj1, obj2) + assert_different_identities(obj1[1], obj2[1]) + + +class MyObject: ... + + +def test_ignores_other_types() -> None: + # custom classes + my_obj = MyObject() + obj1 = {"foo": my_obj} + obj2 = deepcopy_minimal(obj1) + assert_different_identities(obj1, obj2) + assert obj1["foo"] is my_obj + + # tuples + obj3 = ("a", "b") + obj4 = deepcopy_minimal(obj3) + assert obj3 is obj4 diff --git a/tests/test_extract_files.py b/tests/test_extract_files.py new file mode 100644 index 00000000..0d751b46 --- /dev/null +++ b/tests/test_extract_files.py @@ -0,0 +1,64 @@ +from __future__ import annotations + +from typing import Sequence + +import pytest + +from stagehand._types import FileTypes +from stagehand._utils import extract_files + + +def test_removes_files_from_input() -> None: + query = {"foo": "bar"} + assert extract_files(query, paths=[]) == [] + assert query == {"foo": "bar"} + + query2 = {"foo": b"Bar", "hello": "world"} + assert extract_files(query2, paths=[["foo"]]) == [("foo", b"Bar")] + assert query2 == {"hello": "world"} + + query3 = {"foo": {"foo": {"bar": b"Bar"}}, "hello": "world"} + assert extract_files(query3, paths=[["foo", "foo", "bar"]]) == [("foo[foo][bar]", b"Bar")] + assert query3 == {"foo": {"foo": {}}, "hello": "world"} + + query4 = {"foo": {"bar": b"Bar", "baz": "foo"}, "hello": "world"} + assert extract_files(query4, paths=[["foo", "bar"]]) == [("foo[bar]", b"Bar")] + assert query4 == {"hello": "world", "foo": {"baz": "foo"}} + + +def test_multiple_files() -> None: + query = {"documents": [{"file": b"My first file"}, {"file": b"My second file"}]} + assert extract_files(query, paths=[["documents", "", "file"]]) == [ + ("documents[][file]", b"My first file"), + ("documents[][file]", b"My second file"), + ] + assert query == {"documents": [{}, {}]} + + +@pytest.mark.parametrize( + "query,paths,expected", + [ + [ + {"foo": {"bar": "baz"}}, + [["foo", "", "bar"]], + [], + ], + [ + {"foo": ["bar", "baz"]}, + [["foo", "bar"]], + [], + ], + [ + {"foo": {"bar": "baz"}}, + [["foo", "foo"]], + [], + ], + ], + ids=["dict expecting array", "array expecting dict", "unknown keys"], +) +def test_ignores_incorrect_paths( + query: dict[str, object], + paths: Sequence[Sequence[str]], + expected: list[tuple[str, FileTypes]], +) -> None: + assert extract_files(query, paths=paths) == expected diff --git a/tests/test_files.py b/tests/test_files.py new file mode 100644 index 00000000..683b85ee --- /dev/null +++ b/tests/test_files.py @@ -0,0 +1,51 @@ +from pathlib import Path + +import anyio +import pytest +from dirty_equals import IsDict, IsList, IsBytes, IsTuple + +from stagehand._files import to_httpx_files, async_to_httpx_files + +readme_path = Path(__file__).parent.parent.joinpath("README.md") + + +def test_pathlib_includes_file_name() -> None: + result = to_httpx_files({"file": readme_path}) + print(result) + assert result == IsDict({"file": IsTuple("README.md", IsBytes())}) + + +def test_tuple_input() -> None: + result = to_httpx_files([("file", readme_path)]) + print(result) + assert result == IsList(IsTuple("file", IsTuple("README.md", IsBytes()))) + + +@pytest.mark.asyncio +async def test_async_pathlib_includes_file_name() -> None: + result = await async_to_httpx_files({"file": readme_path}) + print(result) + assert result == IsDict({"file": IsTuple("README.md", IsBytes())}) + + +@pytest.mark.asyncio +async def test_async_supports_anyio_path() -> None: + result = await async_to_httpx_files({"file": anyio.Path(readme_path)}) + print(result) + assert result == IsDict({"file": IsTuple("README.md", IsBytes())}) + + +@pytest.mark.asyncio +async def test_async_tuple_input() -> None: + result = await async_to_httpx_files([("file", readme_path)]) + print(result) + assert result == IsList(IsTuple("file", IsTuple("README.md", IsBytes()))) + + +def test_string_not_allowed() -> None: + with pytest.raises(TypeError, match="Expected file types input to be a FileContent type or to be a tuple"): + to_httpx_files( + { + "file": "foo", # type: ignore + } + ) diff --git a/tests/test_models.py b/tests/test_models.py new file mode 100644 index 00000000..d23f1c44 --- /dev/null +++ b/tests/test_models.py @@ -0,0 +1,963 @@ +import json +from typing import TYPE_CHECKING, Any, Dict, List, Union, Optional, cast +from datetime import datetime, timezone +from typing_extensions import Literal, Annotated, TypeAliasType + +import pytest +import pydantic +from pydantic import Field + +from stagehand._utils import PropertyInfo +from stagehand._compat import PYDANTIC_V1, parse_obj, model_dump, model_json +from stagehand._models import DISCRIMINATOR_CACHE, BaseModel, construct_type + + +class BasicModel(BaseModel): + foo: str + + +@pytest.mark.parametrize("value", ["hello", 1], ids=["correct type", "mismatched"]) +def test_basic(value: object) -> None: + m = BasicModel.construct(foo=value) + assert m.foo == value + + +def test_directly_nested_model() -> None: + class NestedModel(BaseModel): + nested: BasicModel + + m = NestedModel.construct(nested={"foo": "Foo!"}) + assert m.nested.foo == "Foo!" + + # mismatched types + m = NestedModel.construct(nested="hello!") + assert cast(Any, m.nested) == "hello!" + + +def test_optional_nested_model() -> None: + class NestedModel(BaseModel): + nested: Optional[BasicModel] + + m1 = NestedModel.construct(nested=None) + assert m1.nested is None + + m2 = NestedModel.construct(nested={"foo": "bar"}) + assert m2.nested is not None + assert m2.nested.foo == "bar" + + # mismatched types + m3 = NestedModel.construct(nested={"foo"}) + assert isinstance(cast(Any, m3.nested), set) + assert cast(Any, m3.nested) == {"foo"} + + +def test_list_nested_model() -> None: + class NestedModel(BaseModel): + nested: List[BasicModel] + + m = NestedModel.construct(nested=[{"foo": "bar"}, {"foo": "2"}]) + assert m.nested is not None + assert isinstance(m.nested, list) + assert len(m.nested) == 2 + assert m.nested[0].foo == "bar" + assert m.nested[1].foo == "2" + + # mismatched types + m = NestedModel.construct(nested=True) + assert cast(Any, m.nested) is True + + m = NestedModel.construct(nested=[False]) + assert cast(Any, m.nested) == [False] + + +def test_optional_list_nested_model() -> None: + class NestedModel(BaseModel): + nested: Optional[List[BasicModel]] + + m1 = NestedModel.construct(nested=[{"foo": "bar"}, {"foo": "2"}]) + assert m1.nested is not None + assert isinstance(m1.nested, list) + assert len(m1.nested) == 2 + assert m1.nested[0].foo == "bar" + assert m1.nested[1].foo == "2" + + m2 = NestedModel.construct(nested=None) + assert m2.nested is None + + # mismatched types + m3 = NestedModel.construct(nested={1}) + assert cast(Any, m3.nested) == {1} + + m4 = NestedModel.construct(nested=[False]) + assert cast(Any, m4.nested) == [False] + + +def test_list_optional_items_nested_model() -> None: + class NestedModel(BaseModel): + nested: List[Optional[BasicModel]] + + m = NestedModel.construct(nested=[None, {"foo": "bar"}]) + assert m.nested is not None + assert isinstance(m.nested, list) + assert len(m.nested) == 2 + assert m.nested[0] is None + assert m.nested[1] is not None + assert m.nested[1].foo == "bar" + + # mismatched types + m3 = NestedModel.construct(nested="foo") + assert cast(Any, m3.nested) == "foo" + + m4 = NestedModel.construct(nested=[False]) + assert cast(Any, m4.nested) == [False] + + +def test_list_mismatched_type() -> None: + class NestedModel(BaseModel): + nested: List[str] + + m = NestedModel.construct(nested=False) + assert cast(Any, m.nested) is False + + +def test_raw_dictionary() -> None: + class NestedModel(BaseModel): + nested: Dict[str, str] + + m = NestedModel.construct(nested={"hello": "world"}) + assert m.nested == {"hello": "world"} + + # mismatched types + m = NestedModel.construct(nested=False) + assert cast(Any, m.nested) is False + + +def test_nested_dictionary_model() -> None: + class NestedModel(BaseModel): + nested: Dict[str, BasicModel] + + m = NestedModel.construct(nested={"hello": {"foo": "bar"}}) + assert isinstance(m.nested, dict) + assert m.nested["hello"].foo == "bar" + + # mismatched types + m = NestedModel.construct(nested={"hello": False}) + assert cast(Any, m.nested["hello"]) is False + + +def test_unknown_fields() -> None: + m1 = BasicModel.construct(foo="foo", unknown=1) + assert m1.foo == "foo" + assert cast(Any, m1).unknown == 1 + + m2 = BasicModel.construct(foo="foo", unknown={"foo_bar": True}) + assert m2.foo == "foo" + assert cast(Any, m2).unknown == {"foo_bar": True} + + assert model_dump(m2) == {"foo": "foo", "unknown": {"foo_bar": True}} + + +def test_strict_validation_unknown_fields() -> None: + class Model(BaseModel): + foo: str + + model = parse_obj(Model, dict(foo="hello!", user="Robert")) + assert model.foo == "hello!" + assert cast(Any, model).user == "Robert" + + assert model_dump(model) == {"foo": "hello!", "user": "Robert"} + + +def test_aliases() -> None: + class Model(BaseModel): + my_field: int = Field(alias="myField") + + m = Model.construct(myField=1) + assert m.my_field == 1 + + # mismatched types + m = Model.construct(myField={"hello": False}) + assert cast(Any, m.my_field) == {"hello": False} + + +def test_repr() -> None: + model = BasicModel(foo="bar") + assert str(model) == "BasicModel(foo='bar')" + assert repr(model) == "BasicModel(foo='bar')" + + +def test_repr_nested_model() -> None: + class Child(BaseModel): + name: str + age: int + + class Parent(BaseModel): + name: str + child: Child + + model = Parent(name="Robert", child=Child(name="Foo", age=5)) + assert str(model) == "Parent(name='Robert', child=Child(name='Foo', age=5))" + assert repr(model) == "Parent(name='Robert', child=Child(name='Foo', age=5))" + + +def test_optional_list() -> None: + class Submodel(BaseModel): + name: str + + class Model(BaseModel): + items: Optional[List[Submodel]] + + m = Model.construct(items=None) + assert m.items is None + + m = Model.construct(items=[]) + assert m.items == [] + + m = Model.construct(items=[{"name": "Robert"}]) + assert m.items is not None + assert len(m.items) == 1 + assert m.items[0].name == "Robert" + + +def test_nested_union_of_models() -> None: + class Submodel1(BaseModel): + bar: bool + + class Submodel2(BaseModel): + thing: str + + class Model(BaseModel): + foo: Union[Submodel1, Submodel2] + + m = Model.construct(foo={"thing": "hello"}) + assert isinstance(m.foo, Submodel2) + assert m.foo.thing == "hello" + + +def test_nested_union_of_mixed_types() -> None: + class Submodel1(BaseModel): + bar: bool + + class Model(BaseModel): + foo: Union[Submodel1, Literal[True], Literal["CARD_HOLDER"]] + + m = Model.construct(foo=True) + assert m.foo is True + + m = Model.construct(foo="CARD_HOLDER") + assert m.foo == "CARD_HOLDER" + + m = Model.construct(foo={"bar": False}) + assert isinstance(m.foo, Submodel1) + assert m.foo.bar is False + + +def test_nested_union_multiple_variants() -> None: + class Submodel1(BaseModel): + bar: bool + + class Submodel2(BaseModel): + thing: str + + class Submodel3(BaseModel): + foo: int + + class Model(BaseModel): + foo: Union[Submodel1, Submodel2, None, Submodel3] + + m = Model.construct(foo={"thing": "hello"}) + assert isinstance(m.foo, Submodel2) + assert m.foo.thing == "hello" + + m = Model.construct(foo=None) + assert m.foo is None + + m = Model.construct() + assert m.foo is None + + m = Model.construct(foo={"foo": "1"}) + assert isinstance(m.foo, Submodel3) + assert m.foo.foo == 1 + + +def test_nested_union_invalid_data() -> None: + class Submodel1(BaseModel): + level: int + + class Submodel2(BaseModel): + name: str + + class Model(BaseModel): + foo: Union[Submodel1, Submodel2] + + m = Model.construct(foo=True) + assert cast(bool, m.foo) is True + + m = Model.construct(foo={"name": 3}) + if PYDANTIC_V1: + assert isinstance(m.foo, Submodel2) + assert m.foo.name == "3" + else: + assert isinstance(m.foo, Submodel1) + assert m.foo.name == 3 # type: ignore + + +def test_list_of_unions() -> None: + class Submodel1(BaseModel): + level: int + + class Submodel2(BaseModel): + name: str + + class Model(BaseModel): + items: List[Union[Submodel1, Submodel2]] + + m = Model.construct(items=[{"level": 1}, {"name": "Robert"}]) + assert len(m.items) == 2 + assert isinstance(m.items[0], Submodel1) + assert m.items[0].level == 1 + assert isinstance(m.items[1], Submodel2) + assert m.items[1].name == "Robert" + + m = Model.construct(items=[{"level": -1}, 156]) + assert len(m.items) == 2 + assert isinstance(m.items[0], Submodel1) + assert m.items[0].level == -1 + assert cast(Any, m.items[1]) == 156 + + +def test_union_of_lists() -> None: + class SubModel1(BaseModel): + level: int + + class SubModel2(BaseModel): + name: str + + class Model(BaseModel): + items: Union[List[SubModel1], List[SubModel2]] + + # with one valid entry + m = Model.construct(items=[{"name": "Robert"}]) + assert len(m.items) == 1 + assert isinstance(m.items[0], SubModel2) + assert m.items[0].name == "Robert" + + # with two entries pointing to different types + m = Model.construct(items=[{"level": 1}, {"name": "Robert"}]) + assert len(m.items) == 2 + assert isinstance(m.items[0], SubModel1) + assert m.items[0].level == 1 + assert isinstance(m.items[1], SubModel1) + assert cast(Any, m.items[1]).name == "Robert" + + # with two entries pointing to *completely* different types + m = Model.construct(items=[{"level": -1}, 156]) + assert len(m.items) == 2 + assert isinstance(m.items[0], SubModel1) + assert m.items[0].level == -1 + assert cast(Any, m.items[1]) == 156 + + +def test_dict_of_union() -> None: + class SubModel1(BaseModel): + name: str + + class SubModel2(BaseModel): + foo: str + + class Model(BaseModel): + data: Dict[str, Union[SubModel1, SubModel2]] + + m = Model.construct(data={"hello": {"name": "there"}, "foo": {"foo": "bar"}}) + assert len(list(m.data.keys())) == 2 + assert isinstance(m.data["hello"], SubModel1) + assert m.data["hello"].name == "there" + assert isinstance(m.data["foo"], SubModel2) + assert m.data["foo"].foo == "bar" + + # TODO: test mismatched type + + +def test_double_nested_union() -> None: + class SubModel1(BaseModel): + name: str + + class SubModel2(BaseModel): + bar: str + + class Model(BaseModel): + data: Dict[str, List[Union[SubModel1, SubModel2]]] + + m = Model.construct(data={"foo": [{"bar": "baz"}, {"name": "Robert"}]}) + assert len(m.data["foo"]) == 2 + + entry1 = m.data["foo"][0] + assert isinstance(entry1, SubModel2) + assert entry1.bar == "baz" + + entry2 = m.data["foo"][1] + assert isinstance(entry2, SubModel1) + assert entry2.name == "Robert" + + # TODO: test mismatched type + + +def test_union_of_dict() -> None: + class SubModel1(BaseModel): + name: str + + class SubModel2(BaseModel): + foo: str + + class Model(BaseModel): + data: Union[Dict[str, SubModel1], Dict[str, SubModel2]] + + m = Model.construct(data={"hello": {"name": "there"}, "foo": {"foo": "bar"}}) + assert len(list(m.data.keys())) == 2 + assert isinstance(m.data["hello"], SubModel1) + assert m.data["hello"].name == "there" + assert isinstance(m.data["foo"], SubModel1) + assert cast(Any, m.data["foo"]).foo == "bar" + + +def test_iso8601_datetime() -> None: + class Model(BaseModel): + created_at: datetime + + expected = datetime(2019, 12, 27, 18, 11, 19, 117000, tzinfo=timezone.utc) + + if PYDANTIC_V1: + expected_json = '{"created_at": "2019-12-27T18:11:19.117000+00:00"}' + else: + expected_json = '{"created_at":"2019-12-27T18:11:19.117000Z"}' + + model = Model.construct(created_at="2019-12-27T18:11:19.117Z") + assert model.created_at == expected + assert model_json(model) == expected_json + + model = parse_obj(Model, dict(created_at="2019-12-27T18:11:19.117Z")) + assert model.created_at == expected + assert model_json(model) == expected_json + + +def test_does_not_coerce_int() -> None: + class Model(BaseModel): + bar: int + + assert Model.construct(bar=1).bar == 1 + assert Model.construct(bar=10.9).bar == 10.9 + assert Model.construct(bar="19").bar == "19" # type: ignore[comparison-overlap] + assert Model.construct(bar=False).bar is False + + +def test_int_to_float_safe_conversion() -> None: + class Model(BaseModel): + float_field: float + + m = Model.construct(float_field=10) + assert m.float_field == 10.0 + assert isinstance(m.float_field, float) + + m = Model.construct(float_field=10.12) + assert m.float_field == 10.12 + assert isinstance(m.float_field, float) + + # number too big + m = Model.construct(float_field=2**53 + 1) + assert m.float_field == 2**53 + 1 + assert isinstance(m.float_field, int) + + +def test_deprecated_alias() -> None: + class Model(BaseModel): + resource_id: str = Field(alias="model_id") + + @property + def model_id(self) -> str: + return self.resource_id + + m = Model.construct(model_id="id") + assert m.model_id == "id" + assert m.resource_id == "id" + assert m.resource_id is m.model_id + + m = parse_obj(Model, {"model_id": "id"}) + assert m.model_id == "id" + assert m.resource_id == "id" + assert m.resource_id is m.model_id + + +def test_omitted_fields() -> None: + class Model(BaseModel): + resource_id: Optional[str] = None + + m = Model.construct() + assert m.resource_id is None + assert "resource_id" not in m.model_fields_set + + m = Model.construct(resource_id=None) + assert m.resource_id is None + assert "resource_id" in m.model_fields_set + + m = Model.construct(resource_id="foo") + assert m.resource_id == "foo" + assert "resource_id" in m.model_fields_set + + +def test_to_dict() -> None: + class Model(BaseModel): + foo: Optional[str] = Field(alias="FOO", default=None) + + m = Model(FOO="hello") + assert m.to_dict() == {"FOO": "hello"} + assert m.to_dict(use_api_names=False) == {"foo": "hello"} + + m2 = Model() + assert m2.to_dict() == {} + assert m2.to_dict(exclude_unset=False) == {"FOO": None} + assert m2.to_dict(exclude_unset=False, exclude_none=True) == {} + assert m2.to_dict(exclude_unset=False, exclude_defaults=True) == {} + + m3 = Model(FOO=None) + assert m3.to_dict() == {"FOO": None} + assert m3.to_dict(exclude_none=True) == {} + assert m3.to_dict(exclude_defaults=True) == {} + + class Model2(BaseModel): + created_at: datetime + + time_str = "2024-03-21T11:39:01.275859" + m4 = Model2.construct(created_at=time_str) + assert m4.to_dict(mode="python") == {"created_at": datetime.fromisoformat(time_str)} + assert m4.to_dict(mode="json") == {"created_at": time_str} + + if PYDANTIC_V1: + with pytest.raises(ValueError, match="warnings is only supported in Pydantic v2"): + m.to_dict(warnings=False) + + +def test_forwards_compat_model_dump_method() -> None: + class Model(BaseModel): + foo: Optional[str] = Field(alias="FOO", default=None) + + m = Model(FOO="hello") + assert m.model_dump() == {"foo": "hello"} + assert m.model_dump(include={"bar"}) == {} + assert m.model_dump(exclude={"foo"}) == {} + assert m.model_dump(by_alias=True) == {"FOO": "hello"} + + m2 = Model() + assert m2.model_dump() == {"foo": None} + assert m2.model_dump(exclude_unset=True) == {} + assert m2.model_dump(exclude_none=True) == {} + assert m2.model_dump(exclude_defaults=True) == {} + + m3 = Model(FOO=None) + assert m3.model_dump() == {"foo": None} + assert m3.model_dump(exclude_none=True) == {} + + if PYDANTIC_V1: + with pytest.raises(ValueError, match="round_trip is only supported in Pydantic v2"): + m.model_dump(round_trip=True) + + with pytest.raises(ValueError, match="warnings is only supported in Pydantic v2"): + m.model_dump(warnings=False) + + +def test_compat_method_no_error_for_warnings() -> None: + class Model(BaseModel): + foo: Optional[str] + + m = Model(foo="hello") + assert isinstance(model_dump(m, warnings=False), dict) + + +def test_to_json() -> None: + class Model(BaseModel): + foo: Optional[str] = Field(alias="FOO", default=None) + + m = Model(FOO="hello") + assert json.loads(m.to_json()) == {"FOO": "hello"} + assert json.loads(m.to_json(use_api_names=False)) == {"foo": "hello"} + + if PYDANTIC_V1: + assert m.to_json(indent=None) == '{"FOO": "hello"}' + else: + assert m.to_json(indent=None) == '{"FOO":"hello"}' + + m2 = Model() + assert json.loads(m2.to_json()) == {} + assert json.loads(m2.to_json(exclude_unset=False)) == {"FOO": None} + assert json.loads(m2.to_json(exclude_unset=False, exclude_none=True)) == {} + assert json.loads(m2.to_json(exclude_unset=False, exclude_defaults=True)) == {} + + m3 = Model(FOO=None) + assert json.loads(m3.to_json()) == {"FOO": None} + assert json.loads(m3.to_json(exclude_none=True)) == {} + + if PYDANTIC_V1: + with pytest.raises(ValueError, match="warnings is only supported in Pydantic v2"): + m.to_json(warnings=False) + + +def test_forwards_compat_model_dump_json_method() -> None: + class Model(BaseModel): + foo: Optional[str] = Field(alias="FOO", default=None) + + m = Model(FOO="hello") + assert json.loads(m.model_dump_json()) == {"foo": "hello"} + assert json.loads(m.model_dump_json(include={"bar"})) == {} + assert json.loads(m.model_dump_json(include={"foo"})) == {"foo": "hello"} + assert json.loads(m.model_dump_json(by_alias=True)) == {"FOO": "hello"} + + assert m.model_dump_json(indent=2) == '{\n "foo": "hello"\n}' + + m2 = Model() + assert json.loads(m2.model_dump_json()) == {"foo": None} + assert json.loads(m2.model_dump_json(exclude_unset=True)) == {} + assert json.loads(m2.model_dump_json(exclude_none=True)) == {} + assert json.loads(m2.model_dump_json(exclude_defaults=True)) == {} + + m3 = Model(FOO=None) + assert json.loads(m3.model_dump_json()) == {"foo": None} + assert json.loads(m3.model_dump_json(exclude_none=True)) == {} + + if PYDANTIC_V1: + with pytest.raises(ValueError, match="round_trip is only supported in Pydantic v2"): + m.model_dump_json(round_trip=True) + + with pytest.raises(ValueError, match="warnings is only supported in Pydantic v2"): + m.model_dump_json(warnings=False) + + +def test_type_compat() -> None: + # our model type can be assigned to Pydantic's model type + + def takes_pydantic(model: pydantic.BaseModel) -> None: # noqa: ARG001 + ... + + class OurModel(BaseModel): + foo: Optional[str] = None + + takes_pydantic(OurModel()) + + +def test_annotated_types() -> None: + class Model(BaseModel): + value: str + + m = construct_type( + value={"value": "foo"}, + type_=cast(Any, Annotated[Model, "random metadata"]), + ) + assert isinstance(m, Model) + assert m.value == "foo" + + +def test_discriminated_unions_invalid_data() -> None: + class A(BaseModel): + type: Literal["a"] + + data: str + + class B(BaseModel): + type: Literal["b"] + + data: int + + m = construct_type( + value={"type": "b", "data": "foo"}, + type_=cast(Any, Annotated[Union[A, B], PropertyInfo(discriminator="type")]), + ) + assert isinstance(m, B) + assert m.type == "b" + assert m.data == "foo" # type: ignore[comparison-overlap] + + m = construct_type( + value={"type": "a", "data": 100}, + type_=cast(Any, Annotated[Union[A, B], PropertyInfo(discriminator="type")]), + ) + assert isinstance(m, A) + assert m.type == "a" + if PYDANTIC_V1: + # pydantic v1 automatically converts inputs to strings + # if the expected type is a str + assert m.data == "100" + else: + assert m.data == 100 # type: ignore[comparison-overlap] + + +def test_discriminated_unions_unknown_variant() -> None: + class A(BaseModel): + type: Literal["a"] + + data: str + + class B(BaseModel): + type: Literal["b"] + + data: int + + m = construct_type( + value={"type": "c", "data": None, "new_thing": "bar"}, + type_=cast(Any, Annotated[Union[A, B], PropertyInfo(discriminator="type")]), + ) + + # just chooses the first variant + assert isinstance(m, A) + assert m.type == "c" # type: ignore[comparison-overlap] + assert m.data == None # type: ignore[unreachable] + assert m.new_thing == "bar" + + +def test_discriminated_unions_invalid_data_nested_unions() -> None: + class A(BaseModel): + type: Literal["a"] + + data: str + + class B(BaseModel): + type: Literal["b"] + + data: int + + class C(BaseModel): + type: Literal["c"] + + data: bool + + m = construct_type( + value={"type": "b", "data": "foo"}, + type_=cast(Any, Annotated[Union[Union[A, B], C], PropertyInfo(discriminator="type")]), + ) + assert isinstance(m, B) + assert m.type == "b" + assert m.data == "foo" # type: ignore[comparison-overlap] + + m = construct_type( + value={"type": "c", "data": "foo"}, + type_=cast(Any, Annotated[Union[Union[A, B], C], PropertyInfo(discriminator="type")]), + ) + assert isinstance(m, C) + assert m.type == "c" + assert m.data == "foo" # type: ignore[comparison-overlap] + + +def test_discriminated_unions_with_aliases_invalid_data() -> None: + class A(BaseModel): + foo_type: Literal["a"] = Field(alias="type") + + data: str + + class B(BaseModel): + foo_type: Literal["b"] = Field(alias="type") + + data: int + + m = construct_type( + value={"type": "b", "data": "foo"}, + type_=cast(Any, Annotated[Union[A, B], PropertyInfo(discriminator="foo_type")]), + ) + assert isinstance(m, B) + assert m.foo_type == "b" + assert m.data == "foo" # type: ignore[comparison-overlap] + + m = construct_type( + value={"type": "a", "data": 100}, + type_=cast(Any, Annotated[Union[A, B], PropertyInfo(discriminator="foo_type")]), + ) + assert isinstance(m, A) + assert m.foo_type == "a" + if PYDANTIC_V1: + # pydantic v1 automatically converts inputs to strings + # if the expected type is a str + assert m.data == "100" + else: + assert m.data == 100 # type: ignore[comparison-overlap] + + +def test_discriminated_unions_overlapping_discriminators_invalid_data() -> None: + class A(BaseModel): + type: Literal["a"] + + data: bool + + class B(BaseModel): + type: Literal["a"] + + data: int + + m = construct_type( + value={"type": "a", "data": "foo"}, + type_=cast(Any, Annotated[Union[A, B], PropertyInfo(discriminator="type")]), + ) + assert isinstance(m, B) + assert m.type == "a" + assert m.data == "foo" # type: ignore[comparison-overlap] + + +def test_discriminated_unions_invalid_data_uses_cache() -> None: + class A(BaseModel): + type: Literal["a"] + + data: str + + class B(BaseModel): + type: Literal["b"] + + data: int + + UnionType = cast(Any, Union[A, B]) + + assert not DISCRIMINATOR_CACHE.get(UnionType) + + m = construct_type( + value={"type": "b", "data": "foo"}, type_=cast(Any, Annotated[UnionType, PropertyInfo(discriminator="type")]) + ) + assert isinstance(m, B) + assert m.type == "b" + assert m.data == "foo" # type: ignore[comparison-overlap] + + discriminator = DISCRIMINATOR_CACHE.get(UnionType) + assert discriminator is not None + + m = construct_type( + value={"type": "b", "data": "foo"}, type_=cast(Any, Annotated[UnionType, PropertyInfo(discriminator="type")]) + ) + assert isinstance(m, B) + assert m.type == "b" + assert m.data == "foo" # type: ignore[comparison-overlap] + + # if the discriminator details object stays the same between invocations then + # we hit the cache + assert DISCRIMINATOR_CACHE.get(UnionType) is discriminator + + +@pytest.mark.skipif(PYDANTIC_V1, reason="TypeAliasType is not supported in Pydantic v1") +def test_type_alias_type() -> None: + Alias = TypeAliasType("Alias", str) # pyright: ignore + + class Model(BaseModel): + alias: Alias + union: Union[int, Alias] + + m = construct_type(value={"alias": "foo", "union": "bar"}, type_=Model) + assert isinstance(m, Model) + assert isinstance(m.alias, str) + assert m.alias == "foo" + assert isinstance(m.union, str) + assert m.union == "bar" + + +@pytest.mark.skipif(PYDANTIC_V1, reason="TypeAliasType is not supported in Pydantic v1") +def test_field_named_cls() -> None: + class Model(BaseModel): + cls: str + + m = construct_type(value={"cls": "foo"}, type_=Model) + assert isinstance(m, Model) + assert isinstance(m.cls, str) + + +def test_discriminated_union_case() -> None: + class A(BaseModel): + type: Literal["a"] + + data: bool + + class B(BaseModel): + type: Literal["b"] + + data: List[Union[A, object]] + + class ModelA(BaseModel): + type: Literal["modelA"] + + data: int + + class ModelB(BaseModel): + type: Literal["modelB"] + + required: str + + data: Union[A, B] + + # when constructing ModelA | ModelB, value data doesn't match ModelB exactly - missing `required` + m = construct_type( + value={"type": "modelB", "data": {"type": "a", "data": True}}, + type_=cast(Any, Annotated[Union[ModelA, ModelB], PropertyInfo(discriminator="type")]), + ) + + assert isinstance(m, ModelB) + + +def test_nested_discriminated_union() -> None: + class InnerType1(BaseModel): + type: Literal["type_1"] + + class InnerModel(BaseModel): + inner_value: str + + class InnerType2(BaseModel): + type: Literal["type_2"] + some_inner_model: InnerModel + + class Type1(BaseModel): + base_type: Literal["base_type_1"] + value: Annotated[ + Union[ + InnerType1, + InnerType2, + ], + PropertyInfo(discriminator="type"), + ] + + class Type2(BaseModel): + base_type: Literal["base_type_2"] + + T = Annotated[ + Union[ + Type1, + Type2, + ], + PropertyInfo(discriminator="base_type"), + ] + + model = construct_type( + type_=T, + value={ + "base_type": "base_type_1", + "value": { + "type": "type_2", + }, + }, + ) + assert isinstance(model, Type1) + assert isinstance(model.value, InnerType2) + + +@pytest.mark.skipif(PYDANTIC_V1, reason="this is only supported in pydantic v2 for now") +def test_extra_properties() -> None: + class Item(BaseModel): + prop: int + + class Model(BaseModel): + __pydantic_extra__: Dict[str, Item] = Field(init=False) # pyright: ignore[reportIncompatibleVariableOverride] + + other: str + + if TYPE_CHECKING: + + def __getattr__(self, attr: str) -> Item: ... + + model = construct_type( + type_=Model, + value={ + "a": {"prop": 1}, + "other": "foo", + }, + ) + assert isinstance(model, Model) + assert model.a.prop == 1 + assert isinstance(model.a, Item) + assert model.other == "foo" diff --git a/tests/test_qs.py b/tests/test_qs.py new file mode 100644 index 00000000..4c62918d --- /dev/null +++ b/tests/test_qs.py @@ -0,0 +1,78 @@ +from typing import Any, cast +from functools import partial +from urllib.parse import unquote + +import pytest + +from stagehand._qs import Querystring, stringify + + +def test_empty() -> None: + assert stringify({}) == "" + assert stringify({"a": {}}) == "" + assert stringify({"a": {"b": {"c": {}}}}) == "" + + +def test_basic() -> None: + assert stringify({"a": 1}) == "a=1" + assert stringify({"a": "b"}) == "a=b" + assert stringify({"a": True}) == "a=true" + assert stringify({"a": False}) == "a=false" + assert stringify({"a": 1.23456}) == "a=1.23456" + assert stringify({"a": None}) == "" + + +@pytest.mark.parametrize("method", ["class", "function"]) +def test_nested_dotted(method: str) -> None: + if method == "class": + serialise = Querystring(nested_format="dots").stringify + else: + serialise = partial(stringify, nested_format="dots") + + assert unquote(serialise({"a": {"b": "c"}})) == "a.b=c" + assert unquote(serialise({"a": {"b": "c", "d": "e", "f": "g"}})) == "a.b=c&a.d=e&a.f=g" + assert unquote(serialise({"a": {"b": {"c": {"d": "e"}}}})) == "a.b.c.d=e" + assert unquote(serialise({"a": {"b": True}})) == "a.b=true" + + +def test_nested_brackets() -> None: + assert unquote(stringify({"a": {"b": "c"}})) == "a[b]=c" + assert unquote(stringify({"a": {"b": "c", "d": "e", "f": "g"}})) == "a[b]=c&a[d]=e&a[f]=g" + assert unquote(stringify({"a": {"b": {"c": {"d": "e"}}}})) == "a[b][c][d]=e" + assert unquote(stringify({"a": {"b": True}})) == "a[b]=true" + + +@pytest.mark.parametrize("method", ["class", "function"]) +def test_array_comma(method: str) -> None: + if method == "class": + serialise = Querystring(array_format="comma").stringify + else: + serialise = partial(stringify, array_format="comma") + + assert unquote(serialise({"in": ["foo", "bar"]})) == "in=foo,bar" + assert unquote(serialise({"a": {"b": [True, False]}})) == "a[b]=true,false" + assert unquote(serialise({"a": {"b": [True, False, None, True]}})) == "a[b]=true,false,true" + + +def test_array_repeat() -> None: + assert unquote(stringify({"in": ["foo", "bar"]})) == "in=foo&in=bar" + assert unquote(stringify({"a": {"b": [True, False]}})) == "a[b]=true&a[b]=false" + assert unquote(stringify({"a": {"b": [True, False, None, True]}})) == "a[b]=true&a[b]=false&a[b]=true" + assert unquote(stringify({"in": ["foo", {"b": {"c": ["d", "e"]}}]})) == "in=foo&in[b][c]=d&in[b][c]=e" + + +@pytest.mark.parametrize("method", ["class", "function"]) +def test_array_brackets(method: str) -> None: + if method == "class": + serialise = Querystring(array_format="brackets").stringify + else: + serialise = partial(stringify, array_format="brackets") + + assert unquote(serialise({"in": ["foo", "bar"]})) == "in[]=foo&in[]=bar" + assert unquote(serialise({"a": {"b": [True, False]}})) == "a[b][]=true&a[b][]=false" + assert unquote(serialise({"a": {"b": [True, False, None, True]}})) == "a[b][]=true&a[b][]=false&a[b][]=true" + + +def test_unknown_array_format() -> None: + with pytest.raises(NotImplementedError, match="Unknown array_format value: foo, choose from comma, repeat"): + stringify({"a": ["foo", "bar"]}, array_format=cast(Any, "foo")) diff --git a/tests/test_required_args.py b/tests/test_required_args.py new file mode 100644 index 00000000..3464e8c4 --- /dev/null +++ b/tests/test_required_args.py @@ -0,0 +1,111 @@ +from __future__ import annotations + +import pytest + +from stagehand._utils import required_args + + +def test_too_many_positional_params() -> None: + @required_args(["a"]) + def foo(a: str | None = None) -> str | None: + return a + + with pytest.raises(TypeError, match=r"foo\(\) takes 1 argument\(s\) but 2 were given"): + foo("a", "b") # type: ignore + + +def test_positional_param() -> None: + @required_args(["a"]) + def foo(a: str | None = None) -> str | None: + return a + + assert foo("a") == "a" + assert foo(None) is None + assert foo(a="b") == "b" + + with pytest.raises(TypeError, match="Missing required argument: 'a'"): + foo() + + +def test_keyword_only_param() -> None: + @required_args(["a"]) + def foo(*, a: str | None = None) -> str | None: + return a + + assert foo(a="a") == "a" + assert foo(a=None) is None + assert foo(a="b") == "b" + + with pytest.raises(TypeError, match="Missing required argument: 'a'"): + foo() + + +def test_multiple_params() -> None: + @required_args(["a", "b", "c"]) + def foo(a: str = "", *, b: str = "", c: str = "") -> str | None: + return f"{a} {b} {c}" + + assert foo(a="a", b="b", c="c") == "a b c" + + error_message = r"Missing required arguments.*" + + with pytest.raises(TypeError, match=error_message): + foo() + + with pytest.raises(TypeError, match=error_message): + foo(a="a") + + with pytest.raises(TypeError, match=error_message): + foo(b="b") + + with pytest.raises(TypeError, match=error_message): + foo(c="c") + + with pytest.raises(TypeError, match=r"Missing required argument: 'a'"): + foo(b="a", c="c") + + with pytest.raises(TypeError, match=r"Missing required argument: 'b'"): + foo("a", c="c") + + +def test_multiple_variants() -> None: + @required_args(["a"], ["b"]) + def foo(*, a: str | None = None, b: str | None = None) -> str | None: + return a if a is not None else b + + assert foo(a="foo") == "foo" + assert foo(b="bar") == "bar" + assert foo(a=None) is None + assert foo(b=None) is None + + # TODO: this error message could probably be improved + with pytest.raises( + TypeError, + match=r"Missing required arguments; Expected either \('a'\) or \('b'\) arguments to be given", + ): + foo() + + +def test_multiple_params_multiple_variants() -> None: + @required_args(["a", "b"], ["c"]) + def foo(*, a: str | None = None, b: str | None = None, c: str | None = None) -> str | None: + if a is not None: + return a + if b is not None: + return b + return c + + error_message = r"Missing required arguments; Expected either \('a' and 'b'\) or \('c'\) arguments to be given" + + with pytest.raises(TypeError, match=error_message): + foo(a="foo") + + with pytest.raises(TypeError, match=error_message): + foo(b="bar") + + with pytest.raises(TypeError, match=error_message): + foo() + + assert foo(a=None, b="bar") == "bar" + assert foo(c=None) is None + assert foo(c="foo") == "foo" diff --git a/tests/test_response.py b/tests/test_response.py new file mode 100644 index 00000000..8da886ce --- /dev/null +++ b/tests/test_response.py @@ -0,0 +1,277 @@ +import json +from typing import Any, List, Union, cast +from typing_extensions import Annotated + +import httpx +import pytest +import pydantic + +from stagehand import BaseModel, Stagehand, AsyncStagehand +from stagehand._response import ( + APIResponse, + BaseAPIResponse, + AsyncAPIResponse, + BinaryAPIResponse, + AsyncBinaryAPIResponse, + extract_response_type, +) +from stagehand._streaming import Stream +from stagehand._base_client import FinalRequestOptions + + +class ConcreteBaseAPIResponse(APIResponse[bytes]): ... + + +class ConcreteAPIResponse(APIResponse[List[str]]): ... + + +class ConcreteAsyncAPIResponse(APIResponse[httpx.Response]): ... + + +def test_extract_response_type_direct_classes() -> None: + assert extract_response_type(BaseAPIResponse[str]) == str + assert extract_response_type(APIResponse[str]) == str + assert extract_response_type(AsyncAPIResponse[str]) == str + + +def test_extract_response_type_direct_class_missing_type_arg() -> None: + with pytest.raises( + RuntimeError, + match="Expected type to have a type argument at index 0 but it did not", + ): + extract_response_type(AsyncAPIResponse) + + +def test_extract_response_type_concrete_subclasses() -> None: + assert extract_response_type(ConcreteBaseAPIResponse) == bytes + assert extract_response_type(ConcreteAPIResponse) == List[str] + assert extract_response_type(ConcreteAsyncAPIResponse) == httpx.Response + + +def test_extract_response_type_binary_response() -> None: + assert extract_response_type(BinaryAPIResponse) == bytes + assert extract_response_type(AsyncBinaryAPIResponse) == bytes + + +class PydanticModel(pydantic.BaseModel): ... + + +def test_response_parse_mismatched_basemodel(client: Stagehand) -> None: + response = APIResponse( + raw=httpx.Response(200, content=b"foo"), + client=client, + stream=False, + stream_cls=None, + cast_to=str, + options=FinalRequestOptions.construct(method="get", url="/foo"), + ) + + with pytest.raises( + TypeError, + match="Pydantic models must subclass our base model type, e.g. `from stagehand import BaseModel`", + ): + response.parse(to=PydanticModel) + + +@pytest.mark.asyncio +async def test_async_response_parse_mismatched_basemodel(async_client: AsyncStagehand) -> None: + response = AsyncAPIResponse( + raw=httpx.Response(200, content=b"foo"), + client=async_client, + stream=False, + stream_cls=None, + cast_to=str, + options=FinalRequestOptions.construct(method="get", url="/foo"), + ) + + with pytest.raises( + TypeError, + match="Pydantic models must subclass our base model type, e.g. `from stagehand import BaseModel`", + ): + await response.parse(to=PydanticModel) + + +def test_response_parse_custom_stream(client: Stagehand) -> None: + response = APIResponse( + raw=httpx.Response(200, content=b"foo"), + client=client, + stream=True, + stream_cls=None, + cast_to=str, + options=FinalRequestOptions.construct(method="get", url="/foo"), + ) + + stream = response.parse(to=Stream[int]) + assert stream._cast_to == int + + +@pytest.mark.asyncio +async def test_async_response_parse_custom_stream(async_client: AsyncStagehand) -> None: + response = AsyncAPIResponse( + raw=httpx.Response(200, content=b"foo"), + client=async_client, + stream=True, + stream_cls=None, + cast_to=str, + options=FinalRequestOptions.construct(method="get", url="/foo"), + ) + + stream = await response.parse(to=Stream[int]) + assert stream._cast_to == int + + +class CustomModel(BaseModel): + foo: str + bar: int + + +def test_response_parse_custom_model(client: Stagehand) -> None: + response = APIResponse( + raw=httpx.Response(200, content=json.dumps({"foo": "hello!", "bar": 2})), + client=client, + stream=False, + stream_cls=None, + cast_to=str, + options=FinalRequestOptions.construct(method="get", url="/foo"), + ) + + obj = response.parse(to=CustomModel) + assert obj.foo == "hello!" + assert obj.bar == 2 + + +@pytest.mark.asyncio +async def test_async_response_parse_custom_model(async_client: AsyncStagehand) -> None: + response = AsyncAPIResponse( + raw=httpx.Response(200, content=json.dumps({"foo": "hello!", "bar": 2})), + client=async_client, + stream=False, + stream_cls=None, + cast_to=str, + options=FinalRequestOptions.construct(method="get", url="/foo"), + ) + + obj = await response.parse(to=CustomModel) + assert obj.foo == "hello!" + assert obj.bar == 2 + + +def test_response_parse_annotated_type(client: Stagehand) -> None: + response = APIResponse( + raw=httpx.Response(200, content=json.dumps({"foo": "hello!", "bar": 2})), + client=client, + stream=False, + stream_cls=None, + cast_to=str, + options=FinalRequestOptions.construct(method="get", url="/foo"), + ) + + obj = response.parse( + to=cast("type[CustomModel]", Annotated[CustomModel, "random metadata"]), + ) + assert obj.foo == "hello!" + assert obj.bar == 2 + + +async def test_async_response_parse_annotated_type(async_client: AsyncStagehand) -> None: + response = AsyncAPIResponse( + raw=httpx.Response(200, content=json.dumps({"foo": "hello!", "bar": 2})), + client=async_client, + stream=False, + stream_cls=None, + cast_to=str, + options=FinalRequestOptions.construct(method="get", url="/foo"), + ) + + obj = await response.parse( + to=cast("type[CustomModel]", Annotated[CustomModel, "random metadata"]), + ) + assert obj.foo == "hello!" + assert obj.bar == 2 + + +@pytest.mark.parametrize( + "content, expected", + [ + ("false", False), + ("true", True), + ("False", False), + ("True", True), + ("TrUe", True), + ("FalSe", False), + ], +) +def test_response_parse_bool(client: Stagehand, content: str, expected: bool) -> None: + response = APIResponse( + raw=httpx.Response(200, content=content), + client=client, + stream=False, + stream_cls=None, + cast_to=str, + options=FinalRequestOptions.construct(method="get", url="/foo"), + ) + + result = response.parse(to=bool) + assert result is expected + + +@pytest.mark.parametrize( + "content, expected", + [ + ("false", False), + ("true", True), + ("False", False), + ("True", True), + ("TrUe", True), + ("FalSe", False), + ], +) +async def test_async_response_parse_bool(client: AsyncStagehand, content: str, expected: bool) -> None: + response = AsyncAPIResponse( + raw=httpx.Response(200, content=content), + client=client, + stream=False, + stream_cls=None, + cast_to=str, + options=FinalRequestOptions.construct(method="get", url="/foo"), + ) + + result = await response.parse(to=bool) + assert result is expected + + +class OtherModel(BaseModel): + a: str + + +@pytest.mark.parametrize("client", [False], indirect=True) # loose validation +def test_response_parse_expect_model_union_non_json_content(client: Stagehand) -> None: + response = APIResponse( + raw=httpx.Response(200, content=b"foo", headers={"Content-Type": "application/text"}), + client=client, + stream=False, + stream_cls=None, + cast_to=str, + options=FinalRequestOptions.construct(method="get", url="/foo"), + ) + + obj = response.parse(to=cast(Any, Union[CustomModel, OtherModel])) + assert isinstance(obj, str) + assert obj == "foo" + + +@pytest.mark.asyncio +@pytest.mark.parametrize("async_client", [False], indirect=True) # loose validation +async def test_async_response_parse_expect_model_union_non_json_content(async_client: AsyncStagehand) -> None: + response = AsyncAPIResponse( + raw=httpx.Response(200, content=b"foo", headers={"Content-Type": "application/text"}), + client=async_client, + stream=False, + stream_cls=None, + cast_to=str, + options=FinalRequestOptions.construct(method="get", url="/foo"), + ) + + obj = await response.parse(to=cast(Any, Union[CustomModel, OtherModel])) + assert isinstance(obj, str) + assert obj == "foo" diff --git a/tests/test_streaming.py b/tests/test_streaming.py new file mode 100644 index 00000000..01423cbb --- /dev/null +++ b/tests/test_streaming.py @@ -0,0 +1,248 @@ +from __future__ import annotations + +from typing import Iterator, AsyncIterator + +import httpx +import pytest + +from stagehand import Stagehand, AsyncStagehand +from stagehand._streaming import Stream, AsyncStream, ServerSentEvent + + +@pytest.mark.asyncio +@pytest.mark.parametrize("sync", [True, False], ids=["sync", "async"]) +async def test_basic(sync: bool, client: Stagehand, async_client: AsyncStagehand) -> None: + def body() -> Iterator[bytes]: + yield b"event: completion\n" + yield b'data: {"foo":true}\n' + yield b"\n" + + iterator = make_event_iterator(content=body(), sync=sync, client=client, async_client=async_client) + + sse = await iter_next(iterator) + assert sse.event == "completion" + assert sse.json() == {"foo": True} + + await assert_empty_iter(iterator) + + +@pytest.mark.asyncio +@pytest.mark.parametrize("sync", [True, False], ids=["sync", "async"]) +async def test_data_missing_event(sync: bool, client: Stagehand, async_client: AsyncStagehand) -> None: + def body() -> Iterator[bytes]: + yield b'data: {"foo":true}\n' + yield b"\n" + + iterator = make_event_iterator(content=body(), sync=sync, client=client, async_client=async_client) + + sse = await iter_next(iterator) + assert sse.event is None + assert sse.json() == {"foo": True} + + await assert_empty_iter(iterator) + + +@pytest.mark.asyncio +@pytest.mark.parametrize("sync", [True, False], ids=["sync", "async"]) +async def test_event_missing_data(sync: bool, client: Stagehand, async_client: AsyncStagehand) -> None: + def body() -> Iterator[bytes]: + yield b"event: ping\n" + yield b"\n" + + iterator = make_event_iterator(content=body(), sync=sync, client=client, async_client=async_client) + + sse = await iter_next(iterator) + assert sse.event == "ping" + assert sse.data == "" + + await assert_empty_iter(iterator) + + +@pytest.mark.asyncio +@pytest.mark.parametrize("sync", [True, False], ids=["sync", "async"]) +async def test_multiple_events(sync: bool, client: Stagehand, async_client: AsyncStagehand) -> None: + def body() -> Iterator[bytes]: + yield b"event: ping\n" + yield b"\n" + yield b"event: completion\n" + yield b"\n" + + iterator = make_event_iterator(content=body(), sync=sync, client=client, async_client=async_client) + + sse = await iter_next(iterator) + assert sse.event == "ping" + assert sse.data == "" + + sse = await iter_next(iterator) + assert sse.event == "completion" + assert sse.data == "" + + await assert_empty_iter(iterator) + + +@pytest.mark.asyncio +@pytest.mark.parametrize("sync", [True, False], ids=["sync", "async"]) +async def test_multiple_events_with_data(sync: bool, client: Stagehand, async_client: AsyncStagehand) -> None: + def body() -> Iterator[bytes]: + yield b"event: ping\n" + yield b'data: {"foo":true}\n' + yield b"\n" + yield b"event: completion\n" + yield b'data: {"bar":false}\n' + yield b"\n" + + iterator = make_event_iterator(content=body(), sync=sync, client=client, async_client=async_client) + + sse = await iter_next(iterator) + assert sse.event == "ping" + assert sse.json() == {"foo": True} + + sse = await iter_next(iterator) + assert sse.event == "completion" + assert sse.json() == {"bar": False} + + await assert_empty_iter(iterator) + + +@pytest.mark.asyncio +@pytest.mark.parametrize("sync", [True, False], ids=["sync", "async"]) +async def test_multiple_data_lines_with_empty_line(sync: bool, client: Stagehand, async_client: AsyncStagehand) -> None: + def body() -> Iterator[bytes]: + yield b"event: ping\n" + yield b"data: {\n" + yield b'data: "foo":\n' + yield b"data: \n" + yield b"data:\n" + yield b"data: true}\n" + yield b"\n\n" + + iterator = make_event_iterator(content=body(), sync=sync, client=client, async_client=async_client) + + sse = await iter_next(iterator) + assert sse.event == "ping" + assert sse.json() == {"foo": True} + assert sse.data == '{\n"foo":\n\n\ntrue}' + + await assert_empty_iter(iterator) + + +@pytest.mark.asyncio +@pytest.mark.parametrize("sync", [True, False], ids=["sync", "async"]) +async def test_data_json_escaped_double_new_line(sync: bool, client: Stagehand, async_client: AsyncStagehand) -> None: + def body() -> Iterator[bytes]: + yield b"event: ping\n" + yield b'data: {"foo": "my long\\n\\ncontent"}' + yield b"\n\n" + + iterator = make_event_iterator(content=body(), sync=sync, client=client, async_client=async_client) + + sse = await iter_next(iterator) + assert sse.event == "ping" + assert sse.json() == {"foo": "my long\n\ncontent"} + + await assert_empty_iter(iterator) + + +@pytest.mark.asyncio +@pytest.mark.parametrize("sync", [True, False], ids=["sync", "async"]) +async def test_multiple_data_lines(sync: bool, client: Stagehand, async_client: AsyncStagehand) -> None: + def body() -> Iterator[bytes]: + yield b"event: ping\n" + yield b"data: {\n" + yield b'data: "foo":\n' + yield b"data: true}\n" + yield b"\n\n" + + iterator = make_event_iterator(content=body(), sync=sync, client=client, async_client=async_client) + + sse = await iter_next(iterator) + assert sse.event == "ping" + assert sse.json() == {"foo": True} + + await assert_empty_iter(iterator) + + +@pytest.mark.parametrize("sync", [True, False], ids=["sync", "async"]) +async def test_special_new_line_character( + sync: bool, + client: Stagehand, + async_client: AsyncStagehand, +) -> None: + def body() -> Iterator[bytes]: + yield b'data: {"content":" culpa"}\n' + yield b"\n" + yield b'data: {"content":" \xe2\x80\xa8"}\n' + yield b"\n" + yield b'data: {"content":"foo"}\n' + yield b"\n" + + iterator = make_event_iterator(content=body(), sync=sync, client=client, async_client=async_client) + + sse = await iter_next(iterator) + assert sse.event is None + assert sse.json() == {"content": " culpa"} + + sse = await iter_next(iterator) + assert sse.event is None + assert sse.json() == {"content": " 
"} + + sse = await iter_next(iterator) + assert sse.event is None + assert sse.json() == {"content": "foo"} + + await assert_empty_iter(iterator) + + +@pytest.mark.parametrize("sync", [True, False], ids=["sync", "async"]) +async def test_multi_byte_character_multiple_chunks( + sync: bool, + client: Stagehand, + async_client: AsyncStagehand, +) -> None: + def body() -> Iterator[bytes]: + yield b'data: {"content":"' + # bytes taken from the string 'известни' and arbitrarily split + # so that some multi-byte characters span multiple chunks + yield b"\xd0" + yield b"\xb8\xd0\xb7\xd0" + yield b"\xb2\xd0\xb5\xd1\x81\xd1\x82\xd0\xbd\xd0\xb8" + yield b'"}\n' + yield b"\n" + + iterator = make_event_iterator(content=body(), sync=sync, client=client, async_client=async_client) + + sse = await iter_next(iterator) + assert sse.event is None + assert sse.json() == {"content": "известни"} + + +async def to_aiter(iter: Iterator[bytes]) -> AsyncIterator[bytes]: + for chunk in iter: + yield chunk + + +async def iter_next(iter: Iterator[ServerSentEvent] | AsyncIterator[ServerSentEvent]) -> ServerSentEvent: + if isinstance(iter, AsyncIterator): + return await iter.__anext__() + + return next(iter) + + +async def assert_empty_iter(iter: Iterator[ServerSentEvent] | AsyncIterator[ServerSentEvent]) -> None: + with pytest.raises((StopAsyncIteration, RuntimeError)): + await iter_next(iter) + + +def make_event_iterator( + content: Iterator[bytes], + *, + sync: bool, + client: Stagehand, + async_client: AsyncStagehand, +) -> Iterator[ServerSentEvent] | AsyncIterator[ServerSentEvent]: + if sync: + return Stream(cast_to=object, client=client, response=httpx.Response(200, content=content))._iter_events() + + return AsyncStream( + cast_to=object, client=async_client, response=httpx.Response(200, content=to_aiter(content)) + )._iter_events() diff --git a/tests/test_transform.py b/tests/test_transform.py new file mode 100644 index 00000000..d963daf8 --- /dev/null +++ b/tests/test_transform.py @@ -0,0 +1,460 @@ +from __future__ import annotations + +import io +import pathlib +from typing import Any, Dict, List, Union, TypeVar, Iterable, Optional, cast +from datetime import date, datetime +from typing_extensions import Required, Annotated, TypedDict + +import pytest + +from stagehand._types import Base64FileInput, omit, not_given +from stagehand._utils import ( + PropertyInfo, + transform as _transform, + parse_datetime, + async_transform as _async_transform, +) +from stagehand._compat import PYDANTIC_V1 +from stagehand._models import BaseModel + +_T = TypeVar("_T") + +SAMPLE_FILE_PATH = pathlib.Path(__file__).parent.joinpath("sample_file.txt") + + +async def transform( + data: _T, + expected_type: object, + use_async: bool, +) -> _T: + if use_async: + return await _async_transform(data, expected_type=expected_type) + + return _transform(data, expected_type=expected_type) + + +parametrize = pytest.mark.parametrize("use_async", [False, True], ids=["sync", "async"]) + + +class Foo1(TypedDict): + foo_bar: Annotated[str, PropertyInfo(alias="fooBar")] + + +@parametrize +@pytest.mark.asyncio +async def test_top_level_alias(use_async: bool) -> None: + assert await transform({"foo_bar": "hello"}, expected_type=Foo1, use_async=use_async) == {"fooBar": "hello"} + + +class Foo2(TypedDict): + bar: Bar2 + + +class Bar2(TypedDict): + this_thing: Annotated[int, PropertyInfo(alias="this__thing")] + baz: Annotated[Baz2, PropertyInfo(alias="Baz")] + + +class Baz2(TypedDict): + my_baz: Annotated[str, PropertyInfo(alias="myBaz")] + + +@parametrize +@pytest.mark.asyncio +async def test_recursive_typeddict(use_async: bool) -> None: + assert await transform({"bar": {"this_thing": 1}}, Foo2, use_async) == {"bar": {"this__thing": 1}} + assert await transform({"bar": {"baz": {"my_baz": "foo"}}}, Foo2, use_async) == {"bar": {"Baz": {"myBaz": "foo"}}} + + +class Foo3(TypedDict): + things: List[Bar3] + + +class Bar3(TypedDict): + my_field: Annotated[str, PropertyInfo(alias="myField")] + + +@parametrize +@pytest.mark.asyncio +async def test_list_of_typeddict(use_async: bool) -> None: + result = await transform({"things": [{"my_field": "foo"}, {"my_field": "foo2"}]}, Foo3, use_async) + assert result == {"things": [{"myField": "foo"}, {"myField": "foo2"}]} + + +class Foo4(TypedDict): + foo: Union[Bar4, Baz4] + + +class Bar4(TypedDict): + foo_bar: Annotated[str, PropertyInfo(alias="fooBar")] + + +class Baz4(TypedDict): + foo_baz: Annotated[str, PropertyInfo(alias="fooBaz")] + + +@parametrize +@pytest.mark.asyncio +async def test_union_of_typeddict(use_async: bool) -> None: + assert await transform({"foo": {"foo_bar": "bar"}}, Foo4, use_async) == {"foo": {"fooBar": "bar"}} + assert await transform({"foo": {"foo_baz": "baz"}}, Foo4, use_async) == {"foo": {"fooBaz": "baz"}} + assert await transform({"foo": {"foo_baz": "baz", "foo_bar": "bar"}}, Foo4, use_async) == { + "foo": {"fooBaz": "baz", "fooBar": "bar"} + } + + +class Foo5(TypedDict): + foo: Annotated[Union[Bar4, List[Baz4]], PropertyInfo(alias="FOO")] + + +class Bar5(TypedDict): + foo_bar: Annotated[str, PropertyInfo(alias="fooBar")] + + +class Baz5(TypedDict): + foo_baz: Annotated[str, PropertyInfo(alias="fooBaz")] + + +@parametrize +@pytest.mark.asyncio +async def test_union_of_list(use_async: bool) -> None: + assert await transform({"foo": {"foo_bar": "bar"}}, Foo5, use_async) == {"FOO": {"fooBar": "bar"}} + assert await transform( + { + "foo": [ + {"foo_baz": "baz"}, + {"foo_baz": "baz"}, + ] + }, + Foo5, + use_async, + ) == {"FOO": [{"fooBaz": "baz"}, {"fooBaz": "baz"}]} + + +class Foo6(TypedDict): + bar: Annotated[str, PropertyInfo(alias="Bar")] + + +@parametrize +@pytest.mark.asyncio +async def test_includes_unknown_keys(use_async: bool) -> None: + assert await transform({"bar": "bar", "baz_": {"FOO": 1}}, Foo6, use_async) == { + "Bar": "bar", + "baz_": {"FOO": 1}, + } + + +class Foo7(TypedDict): + bar: Annotated[List[Bar7], PropertyInfo(alias="bAr")] + foo: Bar7 + + +class Bar7(TypedDict): + foo: str + + +@parametrize +@pytest.mark.asyncio +async def test_ignores_invalid_input(use_async: bool) -> None: + assert await transform({"bar": ""}, Foo7, use_async) == {"bAr": ""} + assert await transform({"foo": ""}, Foo7, use_async) == {"foo": ""} + + +class DatetimeDict(TypedDict, total=False): + foo: Annotated[datetime, PropertyInfo(format="iso8601")] + + bar: Annotated[Optional[datetime], PropertyInfo(format="iso8601")] + + required: Required[Annotated[Optional[datetime], PropertyInfo(format="iso8601")]] + + list_: Required[Annotated[Optional[List[datetime]], PropertyInfo(format="iso8601")]] + + union: Annotated[Union[int, datetime], PropertyInfo(format="iso8601")] + + +class DateDict(TypedDict, total=False): + foo: Annotated[date, PropertyInfo(format="iso8601")] + + +class DatetimeModel(BaseModel): + foo: datetime + + +class DateModel(BaseModel): + foo: Optional[date] + + +@parametrize +@pytest.mark.asyncio +async def test_iso8601_format(use_async: bool) -> None: + dt = datetime.fromisoformat("2023-02-23T14:16:36.337692+00:00") + tz = "+00:00" if PYDANTIC_V1 else "Z" + assert await transform({"foo": dt}, DatetimeDict, use_async) == {"foo": "2023-02-23T14:16:36.337692+00:00"} # type: ignore[comparison-overlap] + assert await transform(DatetimeModel(foo=dt), Any, use_async) == {"foo": "2023-02-23T14:16:36.337692" + tz} # type: ignore[comparison-overlap] + + dt = dt.replace(tzinfo=None) + assert await transform({"foo": dt}, DatetimeDict, use_async) == {"foo": "2023-02-23T14:16:36.337692"} # type: ignore[comparison-overlap] + assert await transform(DatetimeModel(foo=dt), Any, use_async) == {"foo": "2023-02-23T14:16:36.337692"} # type: ignore[comparison-overlap] + + assert await transform({"foo": None}, DateDict, use_async) == {"foo": None} # type: ignore[comparison-overlap] + assert await transform(DateModel(foo=None), Any, use_async) == {"foo": None} # type: ignore + assert await transform({"foo": date.fromisoformat("2023-02-23")}, DateDict, use_async) == {"foo": "2023-02-23"} # type: ignore[comparison-overlap] + assert await transform(DateModel(foo=date.fromisoformat("2023-02-23")), DateDict, use_async) == { + "foo": "2023-02-23" + } # type: ignore[comparison-overlap] + + +@parametrize +@pytest.mark.asyncio +async def test_optional_iso8601_format(use_async: bool) -> None: + dt = datetime.fromisoformat("2023-02-23T14:16:36.337692+00:00") + assert await transform({"bar": dt}, DatetimeDict, use_async) == {"bar": "2023-02-23T14:16:36.337692+00:00"} # type: ignore[comparison-overlap] + + assert await transform({"bar": None}, DatetimeDict, use_async) == {"bar": None} + + +@parametrize +@pytest.mark.asyncio +async def test_required_iso8601_format(use_async: bool) -> None: + dt = datetime.fromisoformat("2023-02-23T14:16:36.337692+00:00") + assert await transform({"required": dt}, DatetimeDict, use_async) == { + "required": "2023-02-23T14:16:36.337692+00:00" + } # type: ignore[comparison-overlap] + + assert await transform({"required": None}, DatetimeDict, use_async) == {"required": None} + + +@parametrize +@pytest.mark.asyncio +async def test_union_datetime(use_async: bool) -> None: + dt = datetime.fromisoformat("2023-02-23T14:16:36.337692+00:00") + assert await transform({"union": dt}, DatetimeDict, use_async) == { # type: ignore[comparison-overlap] + "union": "2023-02-23T14:16:36.337692+00:00" + } + + assert await transform({"union": "foo"}, DatetimeDict, use_async) == {"union": "foo"} + + +@parametrize +@pytest.mark.asyncio +async def test_nested_list_iso6801_format(use_async: bool) -> None: + dt1 = datetime.fromisoformat("2023-02-23T14:16:36.337692+00:00") + dt2 = parse_datetime("2022-01-15T06:34:23Z") + assert await transform({"list_": [dt1, dt2]}, DatetimeDict, use_async) == { # type: ignore[comparison-overlap] + "list_": ["2023-02-23T14:16:36.337692+00:00", "2022-01-15T06:34:23+00:00"] + } + + +@parametrize +@pytest.mark.asyncio +async def test_datetime_custom_format(use_async: bool) -> None: + dt = parse_datetime("2022-01-15T06:34:23Z") + + result = await transform(dt, Annotated[datetime, PropertyInfo(format="custom", format_template="%H")], use_async) + assert result == "06" # type: ignore[comparison-overlap] + + +class DateDictWithRequiredAlias(TypedDict, total=False): + required_prop: Required[Annotated[date, PropertyInfo(format="iso8601", alias="prop")]] + + +@parametrize +@pytest.mark.asyncio +async def test_datetime_with_alias(use_async: bool) -> None: + assert await transform({"required_prop": None}, DateDictWithRequiredAlias, use_async) == {"prop": None} # type: ignore[comparison-overlap] + assert await transform( + {"required_prop": date.fromisoformat("2023-02-23")}, DateDictWithRequiredAlias, use_async + ) == {"prop": "2023-02-23"} # type: ignore[comparison-overlap] + + +class MyModel(BaseModel): + foo: str + + +@parametrize +@pytest.mark.asyncio +async def test_pydantic_model_to_dictionary(use_async: bool) -> None: + assert cast(Any, await transform(MyModel(foo="hi!"), Any, use_async)) == {"foo": "hi!"} + assert cast(Any, await transform(MyModel.construct(foo="hi!"), Any, use_async)) == {"foo": "hi!"} + + +@parametrize +@pytest.mark.asyncio +async def test_pydantic_empty_model(use_async: bool) -> None: + assert cast(Any, await transform(MyModel.construct(), Any, use_async)) == {} + + +@parametrize +@pytest.mark.asyncio +async def test_pydantic_unknown_field(use_async: bool) -> None: + assert cast(Any, await transform(MyModel.construct(my_untyped_field=True), Any, use_async)) == { + "my_untyped_field": True + } + + +@parametrize +@pytest.mark.asyncio +async def test_pydantic_mismatched_types(use_async: bool) -> None: + model = MyModel.construct(foo=True) + if PYDANTIC_V1: + params = await transform(model, Any, use_async) + else: + with pytest.warns(UserWarning): + params = await transform(model, Any, use_async) + assert cast(Any, params) == {"foo": True} + + +@parametrize +@pytest.mark.asyncio +async def test_pydantic_mismatched_object_type(use_async: bool) -> None: + model = MyModel.construct(foo=MyModel.construct(hello="world")) + if PYDANTIC_V1: + params = await transform(model, Any, use_async) + else: + with pytest.warns(UserWarning): + params = await transform(model, Any, use_async) + assert cast(Any, params) == {"foo": {"hello": "world"}} + + +class ModelNestedObjects(BaseModel): + nested: MyModel + + +@parametrize +@pytest.mark.asyncio +async def test_pydantic_nested_objects(use_async: bool) -> None: + model = ModelNestedObjects.construct(nested={"foo": "stainless"}) + assert isinstance(model.nested, MyModel) + assert cast(Any, await transform(model, Any, use_async)) == {"nested": {"foo": "stainless"}} + + +class ModelWithDefaultField(BaseModel): + foo: str + with_none_default: Union[str, None] = None + with_str_default: str = "foo" + + +@parametrize +@pytest.mark.asyncio +async def test_pydantic_default_field(use_async: bool) -> None: + # should be excluded when defaults are used + model = ModelWithDefaultField.construct() + assert model.with_none_default is None + assert model.with_str_default == "foo" + assert cast(Any, await transform(model, Any, use_async)) == {} + + # should be included when the default value is explicitly given + model = ModelWithDefaultField.construct(with_none_default=None, with_str_default="foo") + assert model.with_none_default is None + assert model.with_str_default == "foo" + assert cast(Any, await transform(model, Any, use_async)) == {"with_none_default": None, "with_str_default": "foo"} + + # should be included when a non-default value is explicitly given + model = ModelWithDefaultField.construct(with_none_default="bar", with_str_default="baz") + assert model.with_none_default == "bar" + assert model.with_str_default == "baz" + assert cast(Any, await transform(model, Any, use_async)) == {"with_none_default": "bar", "with_str_default": "baz"} + + +class TypedDictIterableUnion(TypedDict): + foo: Annotated[Union[Bar8, Iterable[Baz8]], PropertyInfo(alias="FOO")] + + +class Bar8(TypedDict): + foo_bar: Annotated[str, PropertyInfo(alias="fooBar")] + + +class Baz8(TypedDict): + foo_baz: Annotated[str, PropertyInfo(alias="fooBaz")] + + +@parametrize +@pytest.mark.asyncio +async def test_iterable_of_dictionaries(use_async: bool) -> None: + assert await transform({"foo": [{"foo_baz": "bar"}]}, TypedDictIterableUnion, use_async) == { + "FOO": [{"fooBaz": "bar"}] + } + assert cast(Any, await transform({"foo": ({"foo_baz": "bar"},)}, TypedDictIterableUnion, use_async)) == { + "FOO": [{"fooBaz": "bar"}] + } + + def my_iter() -> Iterable[Baz8]: + yield {"foo_baz": "hello"} + yield {"foo_baz": "world"} + + assert await transform({"foo": my_iter()}, TypedDictIterableUnion, use_async) == { + "FOO": [{"fooBaz": "hello"}, {"fooBaz": "world"}] + } + + +@parametrize +@pytest.mark.asyncio +async def test_dictionary_items(use_async: bool) -> None: + class DictItems(TypedDict): + foo_baz: Annotated[str, PropertyInfo(alias="fooBaz")] + + assert await transform({"foo": {"foo_baz": "bar"}}, Dict[str, DictItems], use_async) == {"foo": {"fooBaz": "bar"}} + + +class TypedDictIterableUnionStr(TypedDict): + foo: Annotated[Union[str, Iterable[Baz8]], PropertyInfo(alias="FOO")] + + +@parametrize +@pytest.mark.asyncio +async def test_iterable_union_str(use_async: bool) -> None: + assert await transform({"foo": "bar"}, TypedDictIterableUnionStr, use_async) == {"FOO": "bar"} + assert cast(Any, await transform(iter([{"foo_baz": "bar"}]), Union[str, Iterable[Baz8]], use_async)) == [ + {"fooBaz": "bar"} + ] + + +class TypedDictBase64Input(TypedDict): + foo: Annotated[Union[str, Base64FileInput], PropertyInfo(format="base64")] + + +@parametrize +@pytest.mark.asyncio +async def test_base64_file_input(use_async: bool) -> None: + # strings are left as-is + assert await transform({"foo": "bar"}, TypedDictBase64Input, use_async) == {"foo": "bar"} + + # pathlib.Path is automatically converted to base64 + assert await transform({"foo": SAMPLE_FILE_PATH}, TypedDictBase64Input, use_async) == { + "foo": "SGVsbG8sIHdvcmxkIQo=" + } # type: ignore[comparison-overlap] + + # io instances are automatically converted to base64 + assert await transform({"foo": io.StringIO("Hello, world!")}, TypedDictBase64Input, use_async) == { + "foo": "SGVsbG8sIHdvcmxkIQ==" + } # type: ignore[comparison-overlap] + assert await transform({"foo": io.BytesIO(b"Hello, world!")}, TypedDictBase64Input, use_async) == { + "foo": "SGVsbG8sIHdvcmxkIQ==" + } # type: ignore[comparison-overlap] + + +@parametrize +@pytest.mark.asyncio +async def test_transform_skipping(use_async: bool) -> None: + # lists of ints are left as-is + data = [1, 2, 3] + assert await transform(data, List[int], use_async) is data + + # iterables of ints are converted to a list + data = iter([1, 2, 3]) + assert await transform(data, Iterable[int], use_async) == [1, 2, 3] + + +@parametrize +@pytest.mark.asyncio +async def test_strips_notgiven(use_async: bool) -> None: + assert await transform({"foo_bar": "bar"}, Foo1, use_async) == {"fooBar": "bar"} + assert await transform({"foo_bar": not_given}, Foo1, use_async) == {} + + +@parametrize +@pytest.mark.asyncio +async def test_strips_omit(use_async: bool) -> None: + assert await transform({"foo_bar": "bar"}, Foo1, use_async) == {"fooBar": "bar"} + assert await transform({"foo_bar": omit}, Foo1, use_async) == {} diff --git a/tests/test_utils/test_datetime_parse.py b/tests/test_utils/test_datetime_parse.py new file mode 100644 index 00000000..c50e8092 --- /dev/null +++ b/tests/test_utils/test_datetime_parse.py @@ -0,0 +1,110 @@ +""" +Copied from https://github.com/pydantic/pydantic/blob/v1.10.22/tests/test_datetime_parse.py +with modifications so it works without pydantic v1 imports. +""" + +from typing import Type, Union +from datetime import date, datetime, timezone, timedelta + +import pytest + +from stagehand._utils import parse_date, parse_datetime + + +def create_tz(minutes: int) -> timezone: + return timezone(timedelta(minutes=minutes)) + + +@pytest.mark.parametrize( + "value,result", + [ + # Valid inputs + ("1494012444.883309", date(2017, 5, 5)), + (b"1494012444.883309", date(2017, 5, 5)), + (1_494_012_444.883_309, date(2017, 5, 5)), + ("1494012444", date(2017, 5, 5)), + (1_494_012_444, date(2017, 5, 5)), + (0, date(1970, 1, 1)), + ("2012-04-23", date(2012, 4, 23)), + (b"2012-04-23", date(2012, 4, 23)), + ("2012-4-9", date(2012, 4, 9)), + (date(2012, 4, 9), date(2012, 4, 9)), + (datetime(2012, 4, 9, 12, 15), date(2012, 4, 9)), + # Invalid inputs + ("x20120423", ValueError), + ("2012-04-56", ValueError), + (19_999_999_999, date(2603, 10, 11)), # just before watershed + (20_000_000_001, date(1970, 8, 20)), # just after watershed + (1_549_316_052, date(2019, 2, 4)), # nowish in s + (1_549_316_052_104, date(2019, 2, 4)), # nowish in ms + (1_549_316_052_104_324, date(2019, 2, 4)), # nowish in μs + (1_549_316_052_104_324_096, date(2019, 2, 4)), # nowish in ns + ("infinity", date(9999, 12, 31)), + ("inf", date(9999, 12, 31)), + (float("inf"), date(9999, 12, 31)), + ("infinity ", date(9999, 12, 31)), + (int("1" + "0" * 100), date(9999, 12, 31)), + (1e1000, date(9999, 12, 31)), + ("-infinity", date(1, 1, 1)), + ("-inf", date(1, 1, 1)), + ("nan", ValueError), + ], +) +def test_date_parsing(value: Union[str, bytes, int, float], result: Union[date, Type[Exception]]) -> None: + if type(result) == type and issubclass(result, Exception): # pyright: ignore[reportUnnecessaryIsInstance] + with pytest.raises(result): + parse_date(value) + else: + assert parse_date(value) == result + + +@pytest.mark.parametrize( + "value,result", + [ + # Valid inputs + # values in seconds + ("1494012444.883309", datetime(2017, 5, 5, 19, 27, 24, 883_309, tzinfo=timezone.utc)), + (1_494_012_444.883_309, datetime(2017, 5, 5, 19, 27, 24, 883_309, tzinfo=timezone.utc)), + ("1494012444", datetime(2017, 5, 5, 19, 27, 24, tzinfo=timezone.utc)), + (b"1494012444", datetime(2017, 5, 5, 19, 27, 24, tzinfo=timezone.utc)), + (1_494_012_444, datetime(2017, 5, 5, 19, 27, 24, tzinfo=timezone.utc)), + # values in ms + ("1494012444000.883309", datetime(2017, 5, 5, 19, 27, 24, 883, tzinfo=timezone.utc)), + ("-1494012444000.883309", datetime(1922, 8, 29, 4, 32, 35, 999117, tzinfo=timezone.utc)), + (1_494_012_444_000, datetime(2017, 5, 5, 19, 27, 24, tzinfo=timezone.utc)), + ("2012-04-23T09:15:00", datetime(2012, 4, 23, 9, 15)), + ("2012-4-9 4:8:16", datetime(2012, 4, 9, 4, 8, 16)), + ("2012-04-23T09:15:00Z", datetime(2012, 4, 23, 9, 15, 0, 0, timezone.utc)), + ("2012-4-9 4:8:16-0320", datetime(2012, 4, 9, 4, 8, 16, 0, create_tz(-200))), + ("2012-04-23T10:20:30.400+02:30", datetime(2012, 4, 23, 10, 20, 30, 400_000, create_tz(150))), + ("2012-04-23T10:20:30.400+02", datetime(2012, 4, 23, 10, 20, 30, 400_000, create_tz(120))), + ("2012-04-23T10:20:30.400-02", datetime(2012, 4, 23, 10, 20, 30, 400_000, create_tz(-120))), + (b"2012-04-23T10:20:30.400-02", datetime(2012, 4, 23, 10, 20, 30, 400_000, create_tz(-120))), + (datetime(2017, 5, 5), datetime(2017, 5, 5)), + (0, datetime(1970, 1, 1, 0, 0, 0, tzinfo=timezone.utc)), + # Invalid inputs + ("x20120423091500", ValueError), + ("2012-04-56T09:15:90", ValueError), + ("2012-04-23T11:05:00-25:00", ValueError), + (19_999_999_999, datetime(2603, 10, 11, 11, 33, 19, tzinfo=timezone.utc)), # just before watershed + (20_000_000_001, datetime(1970, 8, 20, 11, 33, 20, 1000, tzinfo=timezone.utc)), # just after watershed + (1_549_316_052, datetime(2019, 2, 4, 21, 34, 12, 0, tzinfo=timezone.utc)), # nowish in s + (1_549_316_052_104, datetime(2019, 2, 4, 21, 34, 12, 104_000, tzinfo=timezone.utc)), # nowish in ms + (1_549_316_052_104_324, datetime(2019, 2, 4, 21, 34, 12, 104_324, tzinfo=timezone.utc)), # nowish in μs + (1_549_316_052_104_324_096, datetime(2019, 2, 4, 21, 34, 12, 104_324, tzinfo=timezone.utc)), # nowish in ns + ("infinity", datetime(9999, 12, 31, 23, 59, 59, 999999)), + ("inf", datetime(9999, 12, 31, 23, 59, 59, 999999)), + ("inf ", datetime(9999, 12, 31, 23, 59, 59, 999999)), + (1e50, datetime(9999, 12, 31, 23, 59, 59, 999999)), + (float("inf"), datetime(9999, 12, 31, 23, 59, 59, 999999)), + ("-infinity", datetime(1, 1, 1, 0, 0)), + ("-inf", datetime(1, 1, 1, 0, 0)), + ("nan", ValueError), + ], +) +def test_datetime_parsing(value: Union[str, bytes, int, float], result: Union[datetime, Type[Exception]]) -> None: + if type(result) == type and issubclass(result, Exception): # pyright: ignore[reportUnnecessaryIsInstance] + with pytest.raises(result): + parse_datetime(value) + else: + assert parse_datetime(value) == result diff --git a/tests/test_utils/test_proxy.py b/tests/test_utils/test_proxy.py new file mode 100644 index 00000000..e8707704 --- /dev/null +++ b/tests/test_utils/test_proxy.py @@ -0,0 +1,34 @@ +import operator +from typing import Any +from typing_extensions import override + +from stagehand._utils import LazyProxy + + +class RecursiveLazyProxy(LazyProxy[Any]): + @override + def __load__(self) -> Any: + return self + + def __call__(self, *_args: Any, **_kwds: Any) -> Any: + raise RuntimeError("This should never be called!") + + +def test_recursive_proxy() -> None: + proxy = RecursiveLazyProxy() + assert repr(proxy) == "RecursiveLazyProxy" + assert str(proxy) == "RecursiveLazyProxy" + assert dir(proxy) == [] + assert type(proxy).__name__ == "RecursiveLazyProxy" + assert type(operator.attrgetter("name.foo.bar.baz")(proxy)).__name__ == "RecursiveLazyProxy" + + +def test_isinstance_does_not_error() -> None: + class AlwaysErrorProxy(LazyProxy[Any]): + @override + def __load__(self) -> Any: + raise RuntimeError("Mocking missing dependency") + + proxy = AlwaysErrorProxy() + assert not isinstance(proxy, dict) + assert isinstance(proxy, LazyProxy) diff --git a/tests/test_utils/test_typing.py b/tests/test_utils/test_typing.py new file mode 100644 index 00000000..6e7f0a13 --- /dev/null +++ b/tests/test_utils/test_typing.py @@ -0,0 +1,73 @@ +from __future__ import annotations + +from typing import Generic, TypeVar, cast + +from stagehand._utils import extract_type_var_from_base + +_T = TypeVar("_T") +_T2 = TypeVar("_T2") +_T3 = TypeVar("_T3") + + +class BaseGeneric(Generic[_T]): ... + + +class SubclassGeneric(BaseGeneric[_T]): ... + + +class BaseGenericMultipleTypeArgs(Generic[_T, _T2, _T3]): ... + + +class SubclassGenericMultipleTypeArgs(BaseGenericMultipleTypeArgs[_T, _T2, _T3]): ... + + +class SubclassDifferentOrderGenericMultipleTypeArgs(BaseGenericMultipleTypeArgs[_T2, _T, _T3]): ... + + +def test_extract_type_var() -> None: + assert ( + extract_type_var_from_base( + BaseGeneric[int], + index=0, + generic_bases=cast("tuple[type, ...]", (BaseGeneric,)), + ) + == int + ) + + +def test_extract_type_var_generic_subclass() -> None: + assert ( + extract_type_var_from_base( + SubclassGeneric[int], + index=0, + generic_bases=cast("tuple[type, ...]", (BaseGeneric,)), + ) + == int + ) + + +def test_extract_type_var_multiple() -> None: + typ = BaseGenericMultipleTypeArgs[int, str, None] + + generic_bases = cast("tuple[type, ...]", (BaseGenericMultipleTypeArgs,)) + assert extract_type_var_from_base(typ, index=0, generic_bases=generic_bases) == int + assert extract_type_var_from_base(typ, index=1, generic_bases=generic_bases) == str + assert extract_type_var_from_base(typ, index=2, generic_bases=generic_bases) == type(None) + + +def test_extract_type_var_generic_subclass_multiple() -> None: + typ = SubclassGenericMultipleTypeArgs[int, str, None] + + generic_bases = cast("tuple[type, ...]", (BaseGenericMultipleTypeArgs,)) + assert extract_type_var_from_base(typ, index=0, generic_bases=generic_bases) == int + assert extract_type_var_from_base(typ, index=1, generic_bases=generic_bases) == str + assert extract_type_var_from_base(typ, index=2, generic_bases=generic_bases) == type(None) + + +def test_extract_type_var_generic_subclass_different_ordering_multiple() -> None: + typ = SubclassDifferentOrderGenericMultipleTypeArgs[int, str, None] + + generic_bases = cast("tuple[type, ...]", (BaseGenericMultipleTypeArgs,)) + assert extract_type_var_from_base(typ, index=0, generic_bases=generic_bases) == int + assert extract_type_var_from_base(typ, index=1, generic_bases=generic_bases) == str + assert extract_type_var_from_base(typ, index=2, generic_bases=generic_bases) == type(None) diff --git a/tests/utils.py b/tests/utils.py new file mode 100644 index 00000000..db0231da --- /dev/null +++ b/tests/utils.py @@ -0,0 +1,167 @@ +from __future__ import annotations + +import os +import inspect +import traceback +import contextlib +from typing import Any, TypeVar, Iterator, Sequence, cast +from datetime import date, datetime +from typing_extensions import Literal, get_args, get_origin, assert_type + +from stagehand._types import Omit, NoneType +from stagehand._utils import ( + is_dict, + is_list, + is_list_type, + is_union_type, + extract_type_arg, + is_sequence_type, + is_annotated_type, + is_type_alias_type, +) +from stagehand._compat import PYDANTIC_V1, field_outer_type, get_model_fields +from stagehand._models import BaseModel + +BaseModelT = TypeVar("BaseModelT", bound=BaseModel) + + +def assert_matches_model(model: type[BaseModelT], value: BaseModelT, *, path: list[str]) -> bool: + for name, field in get_model_fields(model).items(): + field_value = getattr(value, name) + if PYDANTIC_V1: + # in v1 nullability was structured differently + # https://docs.pydantic.dev/2.0/migration/#required-optional-and-nullable-fields + allow_none = getattr(field, "allow_none", False) + else: + allow_none = False + + assert_matches_type( + field_outer_type(field), + field_value, + path=[*path, name], + allow_none=allow_none, + ) + + return True + + +# Note: the `path` argument is only used to improve error messages when `--showlocals` is used +def assert_matches_type( + type_: Any, + value: object, + *, + path: list[str], + allow_none: bool = False, +) -> None: + if is_type_alias_type(type_): + type_ = type_.__value__ + + # unwrap `Annotated[T, ...]` -> `T` + if is_annotated_type(type_): + type_ = extract_type_arg(type_, 0) + + if allow_none and value is None: + return + + if type_ is None or type_ is NoneType: + assert value is None + return + + origin = get_origin(type_) or type_ + + if is_list_type(type_): + return _assert_list_type(type_, value) + + if is_sequence_type(type_): + assert isinstance(value, Sequence) + inner_type = get_args(type_)[0] + for entry in value: # type: ignore + assert_type(inner_type, entry) # type: ignore + return + + if origin == str: + assert isinstance(value, str) + elif origin == int: + assert isinstance(value, int) + elif origin == bool: + assert isinstance(value, bool) + elif origin == float: + assert isinstance(value, float) + elif origin == bytes: + assert isinstance(value, bytes) + elif origin == datetime: + assert isinstance(value, datetime) + elif origin == date: + assert isinstance(value, date) + elif origin == object: + # nothing to do here, the expected type is unknown + pass + elif origin == Literal: + assert value in get_args(type_) + elif origin == dict: + assert is_dict(value) + + args = get_args(type_) + key_type = args[0] + items_type = args[1] + + for key, item in value.items(): + assert_matches_type(key_type, key, path=[*path, ""]) + assert_matches_type(items_type, item, path=[*path, ""]) + elif is_union_type(type_): + variants = get_args(type_) + + try: + none_index = variants.index(type(None)) + except ValueError: + pass + else: + # special case Optional[T] for better error messages + if len(variants) == 2: + if value is None: + # valid + return + + return assert_matches_type(type_=variants[not none_index], value=value, path=path) + + for i, variant in enumerate(variants): + try: + assert_matches_type(variant, value, path=[*path, f"variant {i}"]) + return + except AssertionError: + traceback.print_exc() + continue + + raise AssertionError("Did not match any variants") + elif issubclass(origin, BaseModel): + assert isinstance(value, type_) + assert assert_matches_model(type_, cast(Any, value), path=path) + elif inspect.isclass(origin) and origin.__name__ == "HttpxBinaryResponseContent": + assert value.__class__.__name__ == "HttpxBinaryResponseContent" + else: + assert None, f"Unhandled field type: {type_}" + + +def _assert_list_type(type_: type[object], value: object) -> None: + assert is_list(value) + + inner_type = get_args(type_)[0] + for entry in value: + assert_type(inner_type, entry) # type: ignore + + +@contextlib.contextmanager +def update_env(**new_env: str | Omit) -> Iterator[None]: + old = os.environ.copy() + + try: + for name, value in new_env.items(): + if isinstance(value, Omit): + os.environ.pop(name, None) + else: + os.environ[name] = value + + yield None + finally: + os.environ.clear() + os.environ.update(old) From 4110490f753fd37462141489132fa464baf51d8e Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 11 Dec 2025 22:22:32 +0000 Subject: [PATCH 03/88] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index bc4543c7..4a24b465 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 7 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fstagehand-0c12f985340be2a9287e8e01ff8733f7f2d02e019149d1ae95f1a8f8798c6690.yml openapi_spec_hash: efb79934e1dc63763dd4e8493b825273 -config_hash: 03e0374dee89691195d5b8472e6ecc82 +config_hash: 4bf9c91ae4d9270fffff04e62e0c0b0e From c1268bed9bb30f6ffdd4d0963e9ecdda06b21595 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 11 Dec 2025 22:23:05 +0000 Subject: [PATCH 04/88] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index 4a24b465..b2866147 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 7 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fstagehand-0c12f985340be2a9287e8e01ff8733f7f2d02e019149d1ae95f1a8f8798c6690.yml openapi_spec_hash: efb79934e1dc63763dd4e8493b825273 -config_hash: 4bf9c91ae4d9270fffff04e62e0c0b0e +config_hash: 56543e7140dd04c5cc539051f40f0689 From eacbb1ffdc9c05ffc7756b22c2587369f8baf22a Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 11 Dec 2025 22:23:29 +0000 Subject: [PATCH 05/88] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index b2866147..b242466a 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 7 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fstagehand-0c12f985340be2a9287e8e01ff8733f7f2d02e019149d1ae95f1a8f8798c6690.yml openapi_spec_hash: efb79934e1dc63763dd4e8493b825273 -config_hash: 56543e7140dd04c5cc539051f40f0689 +config_hash: 11bbada11e4722208230ed2fab917d9e From 652ddbef9eeada1a7c9726ff5b7a5420a09e1dd4 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 11 Dec 2025 22:23:45 +0000 Subject: [PATCH 06/88] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index b242466a..1714db76 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 7 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fstagehand-0c12f985340be2a9287e8e01ff8733f7f2d02e019149d1ae95f1a8f8798c6690.yml openapi_spec_hash: efb79934e1dc63763dd4e8493b825273 -config_hash: 11bbada11e4722208230ed2fab917d9e +config_hash: 52dbd2eb1e28ad963e5a2bad9d550dc2 From bf97ec36a573b5b996b8a453d7c01b5d28dd130c Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 11 Dec 2025 22:24:06 +0000 Subject: [PATCH 07/88] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index 1714db76..8bf0b375 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 7 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fstagehand-0c12f985340be2a9287e8e01ff8733f7f2d02e019149d1ae95f1a8f8798c6690.yml openapi_spec_hash: efb79934e1dc63763dd4e8493b825273 -config_hash: 52dbd2eb1e28ad963e5a2bad9d550dc2 +config_hash: b96a45d3fa353f5a8f5eb8027acad3ee From 41f02697d5ee56bea90fc567103ff5397e3bd53e Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 11 Dec 2025 22:24:23 +0000 Subject: [PATCH 08/88] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index 8bf0b375..928b213e 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 7 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fstagehand-0c12f985340be2a9287e8e01ff8733f7f2d02e019149d1ae95f1a8f8798c6690.yml openapi_spec_hash: efb79934e1dc63763dd4e8493b825273 -config_hash: b96a45d3fa353f5a8f5eb8027acad3ee +config_hash: 5b4dff5d9fadb8695b4f266e2bab52f8 From 2c14c8c913b74a4ca9c03872693ae05ebbea15d8 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 11 Dec 2025 22:24:39 +0000 Subject: [PATCH 09/88] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index 928b213e..406bd620 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 7 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fstagehand-0c12f985340be2a9287e8e01ff8733f7f2d02e019149d1ae95f1a8f8798c6690.yml openapi_spec_hash: efb79934e1dc63763dd4e8493b825273 -config_hash: 5b4dff5d9fadb8695b4f266e2bab52f8 +config_hash: 516b1f7051c6d71279f347541279821f From e6fa5c6d9e2db8bb6e64541cc084327b2a0abb70 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 11 Dec 2025 22:29:35 +0000 Subject: [PATCH 10/88] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index 406bd620..acffb053 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 7 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fstagehand-0c12f985340be2a9287e8e01ff8733f7f2d02e019149d1ae95f1a8f8798c6690.yml openapi_spec_hash: efb79934e1dc63763dd4e8493b825273 -config_hash: 516b1f7051c6d71279f347541279821f +config_hash: 5f3345d1d825e49f896f3b0e493e6938 From 53301e9e1174cd7b05e6c954a480f1ee4b053f36 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 16 Dec 2025 04:08:21 +0000 Subject: [PATCH 11/88] chore(internal): add missing files argument to base client --- src/stagehand/_base_client.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/stagehand/_base_client.py b/src/stagehand/_base_client.py index e08b1de0..59a19830 100644 --- a/src/stagehand/_base_client.py +++ b/src/stagehand/_base_client.py @@ -1247,9 +1247,12 @@ def patch( *, cast_to: Type[ResponseT], body: Body | None = None, + files: RequestFiles | None = None, options: RequestOptions = {}, ) -> ResponseT: - opts = FinalRequestOptions.construct(method="patch", url=path, json_data=body, **options) + opts = FinalRequestOptions.construct( + method="patch", url=path, json_data=body, files=to_httpx_files(files), **options + ) return self.request(cast_to, opts) def put( @@ -1767,9 +1770,12 @@ async def patch( *, cast_to: Type[ResponseT], body: Body | None = None, + files: RequestFiles | None = None, options: RequestOptions = {}, ) -> ResponseT: - opts = FinalRequestOptions.construct(method="patch", url=path, json_data=body, **options) + opts = FinalRequestOptions.construct( + method="patch", url=path, json_data=body, files=to_httpx_files(files), **options + ) return await self.request(cast_to, opts) async def put( From ba2d624480c92c5efd3a5a6d91e1d91d0711f4c4 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 16 Dec 2025 18:37:23 +0000 Subject: [PATCH 12/88] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index acffb053..67a8e43f 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 7 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fstagehand-0c12f985340be2a9287e8e01ff8733f7f2d02e019149d1ae95f1a8f8798c6690.yml openapi_spec_hash: efb79934e1dc63763dd4e8493b825273 -config_hash: 5f3345d1d825e49f896f3b0e493e6938 +config_hash: cb2b1795c195a63201c8ef7a617934d1 From 58036e9e4785b7470fb0e33a0a1d38e8c03aa86b Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 16 Dec 2025 19:02:59 +0000 Subject: [PATCH 13/88] feat(api): tweak branding and fix some config fields --- .stats.yml | 2 +- LICENSE | 2 +- README.md | 62 +++--- SECURITY.md | 2 +- pyproject.toml | 4 +- src/stagehand/__init__.py | 12 +- src/stagehand/_client.py | 114 +++++------ src/stagehand/_exceptions.py | 4 +- src/stagehand/_resource.py | 10 +- src/stagehand/_response.py | 4 +- src/stagehand/_streaming.py | 6 +- src/stagehand/_utils/_logs.py | 2 +- tests/api_resources/test_sessions.py | 134 ++++++------- tests/conftest.py | 10 +- tests/test_client.py | 276 +++++++++++++-------------- tests/test_response.py | 26 +-- tests/test_streaming.py | 34 ++-- 17 files changed, 342 insertions(+), 362 deletions(-) diff --git a/.stats.yml b/.stats.yml index 67a8e43f..18b7af83 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 7 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fstagehand-0c12f985340be2a9287e8e01ff8733f7f2d02e019149d1ae95f1a8f8798c6690.yml openapi_spec_hash: efb79934e1dc63763dd4e8493b825273 -config_hash: cb2b1795c195a63201c8ef7a617934d1 +config_hash: 1548ab91b7e8621f7fa79e8cff0c3f93 diff --git a/LICENSE b/LICENSE index 6b24314a..2cec9d4b 100644 --- a/LICENSE +++ b/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright 2025 Stagehand + Copyright 2025 Browserbase Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/README.md b/README.md index 1588e9e6..44331a78 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,9 @@ -# Stagehand Python API library +# Browserbase Python API library [![PyPI version](https://img.shields.io/pypi/v/stagehand.svg?label=pypi%20(stable))](https://pypi.org/project/stagehand/) -The Stagehand Python library provides convenient access to the Stagehand REST API from any Python 3.9+ +The Browserbase Python library provides convenient access to the Browserbase REST API from any Python 3.9+ application. The library includes type definitions for all request params and response fields, and offers both synchronous and asynchronous clients powered by [httpx](https://github.com/encode/httpx). @@ -29,12 +29,12 @@ The full API of this library can be found in [api.md](api.md). ```python import os -from stagehand import Stagehand +from stagehand import Browserbase -client = Stagehand( +client = Browserbase( api_key=os.environ.get("STAGEHAND_API_KEY"), # This is the default and can be omitted - # defaults to "production". - environment="environment_1", + # or 'production' | 'local'; defaults to "production". + environment="dev", ) response = client.sessions.start( @@ -50,17 +50,17 @@ so that your API Key is not stored in source control. ## Async usage -Simply import `AsyncStagehand` instead of `Stagehand` and use `await` with each API call: +Simply import `AsyncBrowserbase` instead of `Browserbase` and use `await` with each API call: ```python import os import asyncio -from stagehand import AsyncStagehand +from stagehand import AsyncBrowserbase -client = AsyncStagehand( +client = AsyncBrowserbase( api_key=os.environ.get("STAGEHAND_API_KEY"), # This is the default and can be omitted - # defaults to "production". - environment="environment_1", + # or 'production' | 'local'; defaults to "production". + environment="dev", ) @@ -93,11 +93,11 @@ Then you can enable it by instantiating the client with `http_client=DefaultAioH import os import asyncio from stagehand import DefaultAioHttpClient -from stagehand import AsyncStagehand +from stagehand import AsyncBrowserbase async def main() -> None: - async with AsyncStagehand( + async with AsyncBrowserbase( api_key=os.environ.get("STAGEHAND_API_KEY"), # This is the default and can be omitted http_client=DefaultAioHttpClient(), ) as client: @@ -124,9 +124,9 @@ Typed requests and responses provide autocomplete and documentation within your Nested parameters are dictionaries, typed using `TypedDict`, for example: ```python -from stagehand import Stagehand +from stagehand import Browserbase -client = Stagehand() +client = Browserbase() response = client.sessions.start( env="LOCAL", @@ -146,9 +146,9 @@ All errors inherit from `stagehand.APIError`. ```python import stagehand -from stagehand import Stagehand +from stagehand import Browserbase -client = Stagehand() +client = Browserbase() try: client.sessions.start( @@ -187,10 +187,10 @@ Connection errors (for example, due to a network connectivity problem), 408 Requ You can use the `max_retries` option to configure or disable retry settings: ```python -from stagehand import Stagehand +from stagehand import Browserbase # Configure the default for all requests: -client = Stagehand( +client = Browserbase( # default is 2 max_retries=0, ) @@ -207,16 +207,16 @@ By default requests time out after 1 minute. You can configure this with a `time which accepts a float or an [`httpx.Timeout`](https://www.python-httpx.org/advanced/timeouts/#fine-tuning-the-configuration) object: ```python -from stagehand import Stagehand +from stagehand import Browserbase # Configure the default for all requests: -client = Stagehand( +client = Browserbase( # 20 seconds (default is 1 minute) timeout=20.0, ) # More granular control: -client = Stagehand( +client = Browserbase( timeout=httpx.Timeout(60.0, read=5.0, write=10.0, connect=2.0), ) @@ -236,10 +236,10 @@ Note that requests that time out are [retried twice by default](#retries). We use the standard library [`logging`](https://docs.python.org/3/library/logging.html) module. -You can enable logging by setting the environment variable `STAGEHAND_LOG` to `info`. +You can enable logging by setting the environment variable `BROWSERBASE_LOG` to `info`. ```shell -$ export STAGEHAND_LOG=info +$ export BROWSERBASE_LOG=info ``` Or to `debug` for more verbose logging. @@ -261,9 +261,9 @@ if response.my_field is None: The "raw" Response object can be accessed by prefixing `.with_raw_response.` to any HTTP method call, e.g., ```py -from stagehand import Stagehand +from stagehand import Browserbase -client = Stagehand() +client = Browserbase() response = client.sessions.with_raw_response.start( env="LOCAL", ) @@ -339,10 +339,10 @@ You can directly override the [httpx client](https://www.python-httpx.org/api/#c ```python import httpx -from stagehand import Stagehand, DefaultHttpxClient +from stagehand import Browserbase, DefaultHttpxClient -client = Stagehand( - # Or use the `STAGEHAND_BASE_URL` env var +client = Browserbase( + # Or use the `BROWSERBASE_BASE_URL` env var base_url="http://my.test.server.example.com:8083", http_client=DefaultHttpxClient( proxy="http://my.test.proxy.example.com", @@ -362,9 +362,9 @@ client.with_options(http_client=DefaultHttpxClient(...)) By default the library closes underlying HTTP connections whenever the client is [garbage collected](https://docs.python.org/3/reference/datamodel.html#object.__del__). You can manually close the client using the `.close()` method if desired, or with a context manager that closes when exiting. ```py -from stagehand import Stagehand +from stagehand import Browserbase -with Stagehand() as client: +with Browserbase() as client: # make requests here ... diff --git a/SECURITY.md b/SECURITY.md index dcfc419a..be1c2db7 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -16,7 +16,7 @@ before making any information public. ## Reporting Non-SDK Related Security Issues If you encounter security issues that are not directly related to SDKs but pertain to the services -or products provided by Stagehand, please follow the respective company's security reporting guidelines. +or products provided by Browserbase, please follow the respective company's security reporting guidelines. --- diff --git a/pyproject.toml b/pyproject.toml index aa099b61..c26da1f7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,11 +1,11 @@ [project] name = "stagehand" version = "0.0.1" -description = "The official Python library for the stagehand API" +description = "The official Python library for the browserbase API" dynamic = ["readme"] license = "Apache-2.0" authors = [ -{ name = "Stagehand", email = "" }, +{ name = "Browserbase", email = "" }, ] dependencies = [ diff --git a/src/stagehand/__init__.py b/src/stagehand/__init__.py index 44c6dfcc..ed545a55 100644 --- a/src/stagehand/__init__.py +++ b/src/stagehand/__init__.py @@ -10,12 +10,12 @@ Client, Stream, Timeout, - Stagehand, Transport, AsyncClient, AsyncStream, - AsyncStagehand, + Browserbase, RequestOptions, + AsyncBrowserbase, ) from ._models import BaseModel from ._version import __title__, __version__ @@ -27,9 +27,9 @@ NotFoundError, APIStatusError, RateLimitError, - StagehandError, APITimeoutError, BadRequestError, + BrowserbaseError, APIConnectionError, AuthenticationError, InternalServerError, @@ -52,7 +52,7 @@ "not_given", "Omit", "omit", - "StagehandError", + "BrowserbaseError", "APIError", "APIStatusError", "APITimeoutError", @@ -72,8 +72,8 @@ "AsyncClient", "Stream", "AsyncStream", - "Stagehand", - "AsyncStagehand", + "Browserbase", + "AsyncBrowserbase", "ENVIRONMENTS", "file_from_path", "BaseModel", diff --git a/src/stagehand/_client.py b/src/stagehand/_client.py index bf4d9773..d5773107 100644 --- a/src/stagehand/_client.py +++ b/src/stagehand/_client.py @@ -12,7 +12,6 @@ from ._qs import Querystring from ._types import ( Omit, - Headers, Timeout, NotGiven, Transport, @@ -24,7 +23,7 @@ from ._version import __version__ from .resources import sessions from ._streaming import Stream as Stream, AsyncStream as AsyncStream -from ._exceptions import APIStatusError +from ._exceptions import APIStatusError, BrowserbaseError from ._base_client import ( DEFAULT_MAX_RETRIES, SyncAPIClient, @@ -37,33 +36,34 @@ "Transport", "ProxiesTypes", "RequestOptions", - "Stagehand", - "AsyncStagehand", + "Browserbase", + "AsyncBrowserbase", "Client", "AsyncClient", ] ENVIRONMENTS: Dict[str, str] = { - "production": "http://localhost:3000/v1", - "environment_1": "https://api.stagehand.browserbase.com/v1", + "production": "https://api.stagehand.browserbase.com/v1", + "dev": "https://api.stagehand.dev.browserbase.com/v1", + "local": "http://localhost:5000/v1", } -class Stagehand(SyncAPIClient): +class Browserbase(SyncAPIClient): sessions: sessions.SessionsResource - with_raw_response: StagehandWithRawResponse - with_streaming_response: StagehandWithStreamedResponse + with_raw_response: BrowserbaseWithRawResponse + with_streaming_response: BrowserbaseWithStreamedResponse # client options - api_key: str | None + api_key: str - _environment: Literal["production", "environment_1"] | NotGiven + _environment: Literal["production", "dev", "local"] | NotGiven def __init__( self, *, api_key: str | None = None, - environment: Literal["production", "environment_1"] | NotGiven = not_given, + environment: Literal["production", "dev", "local"] | NotGiven = not_given, base_url: str | httpx.URL | None | NotGiven = not_given, timeout: float | Timeout | None | NotGiven = not_given, max_retries: int = DEFAULT_MAX_RETRIES, @@ -83,24 +83,28 @@ def __init__( # part of our public interface in the future. _strict_response_validation: bool = False, ) -> None: - """Construct a new synchronous Stagehand client instance. + """Construct a new synchronous Browserbase client instance. This automatically infers the `api_key` argument from the `STAGEHAND_API_KEY` environment variable if it is not provided. """ if api_key is None: api_key = os.environ.get("STAGEHAND_API_KEY") + if api_key is None: + raise BrowserbaseError( + "The api_key client option must be set either by passing api_key to the client or by setting the STAGEHAND_API_KEY environment variable" + ) self.api_key = api_key self._environment = environment - base_url_env = os.environ.get("STAGEHAND_BASE_URL") + base_url_env = os.environ.get("BROWSERBASE_BASE_URL") if is_given(base_url) and base_url is not None: # cast required because mypy doesn't understand the type narrowing base_url = cast("str | httpx.URL", base_url) # pyright: ignore[reportUnnecessaryCast] elif is_given(environment): if base_url_env and base_url is not None: raise ValueError( - "Ambiguous URL; The `STAGEHAND_BASE_URL` env var and the `environment` argument are given. If you want to use the environment, you must pass base_url=None", + "Ambiguous URL; The `BROWSERBASE_BASE_URL` env var and the `environment` argument are given. If you want to use the environment, you must pass base_url=None", ) try: @@ -129,8 +133,8 @@ def __init__( ) self.sessions = sessions.SessionsResource(self) - self.with_raw_response = StagehandWithRawResponse(self) - self.with_streaming_response = StagehandWithStreamedResponse(self) + self.with_raw_response = BrowserbaseWithRawResponse(self) + self.with_streaming_response = BrowserbaseWithStreamedResponse(self) @property @override @@ -141,8 +145,6 @@ def qs(self) -> Querystring: @override def auth_headers(self) -> dict[str, str]: api_key = self.api_key - if api_key is None: - return {} return {"Authorization": f"Bearer {api_key}"} @property @@ -154,22 +156,11 @@ def default_headers(self) -> dict[str, str | Omit]: **self._custom_headers, } - @override - def _validate_headers(self, headers: Headers, custom_headers: Headers) -> None: - if self.api_key and headers.get("Authorization"): - return - if isinstance(custom_headers.get("Authorization"), Omit): - return - - raise TypeError( - '"Could not resolve authentication method. Expected the api_key to be set. Or for the `Authorization` headers to be explicitly omitted"' - ) - def copy( self, *, api_key: str | None = None, - environment: Literal["production", "environment_1"] | None = None, + environment: Literal["production", "dev", "local"] | None = None, base_url: str | httpx.URL | None = None, timeout: float | Timeout | None | NotGiven = not_given, http_client: httpx.Client | None = None, @@ -252,21 +243,21 @@ def _make_status_error( return APIStatusError(err_msg, response=response, body=body) -class AsyncStagehand(AsyncAPIClient): +class AsyncBrowserbase(AsyncAPIClient): sessions: sessions.AsyncSessionsResource - with_raw_response: AsyncStagehandWithRawResponse - with_streaming_response: AsyncStagehandWithStreamedResponse + with_raw_response: AsyncBrowserbaseWithRawResponse + with_streaming_response: AsyncBrowserbaseWithStreamedResponse # client options - api_key: str | None + api_key: str - _environment: Literal["production", "environment_1"] | NotGiven + _environment: Literal["production", "dev", "local"] | NotGiven def __init__( self, *, api_key: str | None = None, - environment: Literal["production", "environment_1"] | NotGiven = not_given, + environment: Literal["production", "dev", "local"] | NotGiven = not_given, base_url: str | httpx.URL | None | NotGiven = not_given, timeout: float | Timeout | None | NotGiven = not_given, max_retries: int = DEFAULT_MAX_RETRIES, @@ -286,24 +277,28 @@ def __init__( # part of our public interface in the future. _strict_response_validation: bool = False, ) -> None: - """Construct a new async AsyncStagehand client instance. + """Construct a new async AsyncBrowserbase client instance. This automatically infers the `api_key` argument from the `STAGEHAND_API_KEY` environment variable if it is not provided. """ if api_key is None: api_key = os.environ.get("STAGEHAND_API_KEY") + if api_key is None: + raise BrowserbaseError( + "The api_key client option must be set either by passing api_key to the client or by setting the STAGEHAND_API_KEY environment variable" + ) self.api_key = api_key self._environment = environment - base_url_env = os.environ.get("STAGEHAND_BASE_URL") + base_url_env = os.environ.get("BROWSERBASE_BASE_URL") if is_given(base_url) and base_url is not None: # cast required because mypy doesn't understand the type narrowing base_url = cast("str | httpx.URL", base_url) # pyright: ignore[reportUnnecessaryCast] elif is_given(environment): if base_url_env and base_url is not None: raise ValueError( - "Ambiguous URL; The `STAGEHAND_BASE_URL` env var and the `environment` argument are given. If you want to use the environment, you must pass base_url=None", + "Ambiguous URL; The `BROWSERBASE_BASE_URL` env var and the `environment` argument are given. If you want to use the environment, you must pass base_url=None", ) try: @@ -332,8 +327,8 @@ def __init__( ) self.sessions = sessions.AsyncSessionsResource(self) - self.with_raw_response = AsyncStagehandWithRawResponse(self) - self.with_streaming_response = AsyncStagehandWithStreamedResponse(self) + self.with_raw_response = AsyncBrowserbaseWithRawResponse(self) + self.with_streaming_response = AsyncBrowserbaseWithStreamedResponse(self) @property @override @@ -344,8 +339,6 @@ def qs(self) -> Querystring: @override def auth_headers(self) -> dict[str, str]: api_key = self.api_key - if api_key is None: - return {} return {"Authorization": f"Bearer {api_key}"} @property @@ -357,22 +350,11 @@ def default_headers(self) -> dict[str, str | Omit]: **self._custom_headers, } - @override - def _validate_headers(self, headers: Headers, custom_headers: Headers) -> None: - if self.api_key and headers.get("Authorization"): - return - if isinstance(custom_headers.get("Authorization"), Omit): - return - - raise TypeError( - '"Could not resolve authentication method. Expected the api_key to be set. Or for the `Authorization` headers to be explicitly omitted"' - ) - def copy( self, *, api_key: str | None = None, - environment: Literal["production", "environment_1"] | None = None, + environment: Literal["production", "dev", "local"] | None = None, base_url: str | httpx.URL | None = None, timeout: float | Timeout | None | NotGiven = not_given, http_client: httpx.AsyncClient | None = None, @@ -455,26 +437,26 @@ def _make_status_error( return APIStatusError(err_msg, response=response, body=body) -class StagehandWithRawResponse: - def __init__(self, client: Stagehand) -> None: +class BrowserbaseWithRawResponse: + def __init__(self, client: Browserbase) -> None: self.sessions = sessions.SessionsResourceWithRawResponse(client.sessions) -class AsyncStagehandWithRawResponse: - def __init__(self, client: AsyncStagehand) -> None: +class AsyncBrowserbaseWithRawResponse: + def __init__(self, client: AsyncBrowserbase) -> None: self.sessions = sessions.AsyncSessionsResourceWithRawResponse(client.sessions) -class StagehandWithStreamedResponse: - def __init__(self, client: Stagehand) -> None: +class BrowserbaseWithStreamedResponse: + def __init__(self, client: Browserbase) -> None: self.sessions = sessions.SessionsResourceWithStreamingResponse(client.sessions) -class AsyncStagehandWithStreamedResponse: - def __init__(self, client: AsyncStagehand) -> None: +class AsyncBrowserbaseWithStreamedResponse: + def __init__(self, client: AsyncBrowserbase) -> None: self.sessions = sessions.AsyncSessionsResourceWithStreamingResponse(client.sessions) -Client = Stagehand +Client = Browserbase -AsyncClient = AsyncStagehand +AsyncClient = AsyncBrowserbase diff --git a/src/stagehand/_exceptions.py b/src/stagehand/_exceptions.py index 52ee0590..79b18ef7 100644 --- a/src/stagehand/_exceptions.py +++ b/src/stagehand/_exceptions.py @@ -18,11 +18,11 @@ ] -class StagehandError(Exception): +class BrowserbaseError(Exception): pass -class APIError(StagehandError): +class APIError(BrowserbaseError): message: str request: httpx.Request diff --git a/src/stagehand/_resource.py b/src/stagehand/_resource.py index 0bdbdf73..fc2d2a11 100644 --- a/src/stagehand/_resource.py +++ b/src/stagehand/_resource.py @@ -8,13 +8,13 @@ import anyio if TYPE_CHECKING: - from ._client import Stagehand, AsyncStagehand + from ._client import Browserbase, AsyncBrowserbase class SyncAPIResource: - _client: Stagehand + _client: Browserbase - def __init__(self, client: Stagehand) -> None: + def __init__(self, client: Browserbase) -> None: self._client = client self._get = client.get self._post = client.post @@ -28,9 +28,9 @@ def _sleep(self, seconds: float) -> None: class AsyncAPIResource: - _client: AsyncStagehand + _client: AsyncBrowserbase - def __init__(self, client: AsyncStagehand) -> None: + def __init__(self, client: AsyncBrowserbase) -> None: self._client = client self._get = client.get self._post = client.post diff --git a/src/stagehand/_response.py b/src/stagehand/_response.py index d1003417..4c4f12e9 100644 --- a/src/stagehand/_response.py +++ b/src/stagehand/_response.py @@ -29,7 +29,7 @@ from ._models import BaseModel, is_basemodel from ._constants import RAW_RESPONSE_HEADER, OVERRIDE_CAST_TO_HEADER from ._streaming import Stream, AsyncStream, is_stream_class_type, extract_stream_chunk_type -from ._exceptions import StagehandError, APIResponseValidationError +from ._exceptions import BrowserbaseError, APIResponseValidationError if TYPE_CHECKING: from ._models import FinalRequestOptions @@ -560,7 +560,7 @@ def __init__(self) -> None: ) -class StreamAlreadyConsumed(StagehandError): +class StreamAlreadyConsumed(BrowserbaseError): """ Attempted to read or stream content, but the content has already been streamed. diff --git a/src/stagehand/_streaming.py b/src/stagehand/_streaming.py index 69a9442b..d107619d 100644 --- a/src/stagehand/_streaming.py +++ b/src/stagehand/_streaming.py @@ -12,7 +12,7 @@ from ._utils import extract_type_var_from_base if TYPE_CHECKING: - from ._client import Stagehand, AsyncStagehand + from ._client import Browserbase, AsyncBrowserbase _T = TypeVar("_T") @@ -30,7 +30,7 @@ def __init__( *, cast_to: type[_T], response: httpx.Response, - client: Stagehand, + client: Browserbase, ) -> None: self.response = response self._cast_to = cast_to @@ -93,7 +93,7 @@ def __init__( *, cast_to: type[_T], response: httpx.Response, - client: AsyncStagehand, + client: AsyncBrowserbase, ) -> None: self.response = response self._cast_to = cast_to diff --git a/src/stagehand/_utils/_logs.py b/src/stagehand/_utils/_logs.py index 2d011968..a8324d9d 100644 --- a/src/stagehand/_utils/_logs.py +++ b/src/stagehand/_utils/_logs.py @@ -14,7 +14,7 @@ def _basic_config() -> None: def setup_logging() -> None: - env = os.environ.get("STAGEHAND_LOG") + env = os.environ.get("BROWSERBASE_LOG") if env == "debug": _basic_config() logger.setLevel(logging.DEBUG) diff --git a/tests/api_resources/test_sessions.py b/tests/api_resources/test_sessions.py index 2c0829d5..c538f244 100644 --- a/tests/api_resources/test_sessions.py +++ b/tests/api_resources/test_sessions.py @@ -7,7 +7,7 @@ import pytest -from stagehand import Stagehand, AsyncStagehand +from stagehand import Browserbase, AsyncBrowserbase from tests.utils import assert_matches_type from stagehand.types import ( SessionActResponse, @@ -27,7 +27,7 @@ class TestSessions: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - def test_method_act(self, client: Stagehand) -> None: + def test_method_act(self, client: Browserbase) -> None: session = client.sessions.act( session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", input="click the sign in button", @@ -36,7 +36,7 @@ def test_method_act(self, client: Stagehand) -> None: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - def test_method_act_with_all_params(self, client: Stagehand) -> None: + def test_method_act_with_all_params(self, client: Browserbase) -> None: session = client.sessions.act( session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", input="click the sign in button", @@ -57,7 +57,7 @@ def test_method_act_with_all_params(self, client: Stagehand) -> None: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - def test_raw_response_act(self, client: Stagehand) -> None: + def test_raw_response_act(self, client: Browserbase) -> None: response = client.sessions.with_raw_response.act( session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", input="click the sign in button", @@ -70,7 +70,7 @@ def test_raw_response_act(self, client: Stagehand) -> None: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - def test_streaming_response_act(self, client: Stagehand) -> None: + def test_streaming_response_act(self, client: Browserbase) -> None: with client.sessions.with_streaming_response.act( session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", input="click the sign in button", @@ -85,7 +85,7 @@ def test_streaming_response_act(self, client: Stagehand) -> None: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - def test_path_params_act(self, client: Stagehand) -> None: + def test_path_params_act(self, client: Browserbase) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `session_id` but received ''"): client.sessions.with_raw_response.act( session_id="", @@ -94,7 +94,7 @@ def test_path_params_act(self, client: Stagehand) -> None: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - def test_method_end(self, client: Stagehand) -> None: + def test_method_end(self, client: Browserbase) -> None: session = client.sessions.end( "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", ) @@ -102,7 +102,7 @@ def test_method_end(self, client: Stagehand) -> None: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - def test_raw_response_end(self, client: Stagehand) -> None: + def test_raw_response_end(self, client: Browserbase) -> None: response = client.sessions.with_raw_response.end( "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", ) @@ -114,7 +114,7 @@ def test_raw_response_end(self, client: Stagehand) -> None: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - def test_streaming_response_end(self, client: Stagehand) -> None: + def test_streaming_response_end(self, client: Browserbase) -> None: with client.sessions.with_streaming_response.end( "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", ) as response: @@ -128,7 +128,7 @@ def test_streaming_response_end(self, client: Stagehand) -> None: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - def test_path_params_end(self, client: Stagehand) -> None: + def test_path_params_end(self, client: Browserbase) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `session_id` but received ''"): client.sessions.with_raw_response.end( "", @@ -136,7 +136,7 @@ def test_path_params_end(self, client: Stagehand) -> None: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - def test_method_execute_agent(self, client: Stagehand) -> None: + def test_method_execute_agent(self, client: Browserbase) -> None: session = client.sessions.execute_agent( session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", agent_config={}, @@ -146,7 +146,7 @@ def test_method_execute_agent(self, client: Stagehand) -> None: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - def test_method_execute_agent_with_all_params(self, client: Stagehand) -> None: + def test_method_execute_agent_with_all_params(self, client: Browserbase) -> None: session = client.sessions.execute_agent( session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", agent_config={ @@ -167,7 +167,7 @@ def test_method_execute_agent_with_all_params(self, client: Stagehand) -> None: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - def test_raw_response_execute_agent(self, client: Stagehand) -> None: + def test_raw_response_execute_agent(self, client: Browserbase) -> None: response = client.sessions.with_raw_response.execute_agent( session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", agent_config={}, @@ -181,7 +181,7 @@ def test_raw_response_execute_agent(self, client: Stagehand) -> None: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - def test_streaming_response_execute_agent(self, client: Stagehand) -> None: + def test_streaming_response_execute_agent(self, client: Browserbase) -> None: with client.sessions.with_streaming_response.execute_agent( session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", agent_config={}, @@ -197,7 +197,7 @@ def test_streaming_response_execute_agent(self, client: Stagehand) -> None: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - def test_path_params_execute_agent(self, client: Stagehand) -> None: + def test_path_params_execute_agent(self, client: Browserbase) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `session_id` but received ''"): client.sessions.with_raw_response.execute_agent( session_id="", @@ -207,7 +207,7 @@ def test_path_params_execute_agent(self, client: Stagehand) -> None: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - def test_method_extract(self, client: Stagehand) -> None: + def test_method_extract(self, client: Browserbase) -> None: session = client.sessions.extract( session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", ) @@ -215,7 +215,7 @@ def test_method_extract(self, client: Stagehand) -> None: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - def test_method_extract_with_all_params(self, client: Stagehand) -> None: + def test_method_extract_with_all_params(self, client: Browserbase) -> None: session = client.sessions.extract( session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", frame_id="frameId", @@ -237,7 +237,7 @@ def test_method_extract_with_all_params(self, client: Stagehand) -> None: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - def test_raw_response_extract(self, client: Stagehand) -> None: + def test_raw_response_extract(self, client: Browserbase) -> None: response = client.sessions.with_raw_response.extract( session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", ) @@ -249,7 +249,7 @@ def test_raw_response_extract(self, client: Stagehand) -> None: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - def test_streaming_response_extract(self, client: Stagehand) -> None: + def test_streaming_response_extract(self, client: Browserbase) -> None: with client.sessions.with_streaming_response.extract( session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", ) as response: @@ -263,7 +263,7 @@ def test_streaming_response_extract(self, client: Stagehand) -> None: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - def test_path_params_extract(self, client: Stagehand) -> None: + def test_path_params_extract(self, client: Browserbase) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `session_id` but received ''"): client.sessions.with_raw_response.extract( session_id="", @@ -271,7 +271,7 @@ def test_path_params_extract(self, client: Stagehand) -> None: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - def test_method_navigate(self, client: Stagehand) -> None: + def test_method_navigate(self, client: Browserbase) -> None: session = client.sessions.navigate( session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", url="https://example.com", @@ -280,7 +280,7 @@ def test_method_navigate(self, client: Stagehand) -> None: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - def test_method_navigate_with_all_params(self, client: Stagehand) -> None: + def test_method_navigate_with_all_params(self, client: Browserbase) -> None: session = client.sessions.navigate( session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", url="https://example.com", @@ -292,7 +292,7 @@ def test_method_navigate_with_all_params(self, client: Stagehand) -> None: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - def test_raw_response_navigate(self, client: Stagehand) -> None: + def test_raw_response_navigate(self, client: Browserbase) -> None: response = client.sessions.with_raw_response.navigate( session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", url="https://example.com", @@ -305,7 +305,7 @@ def test_raw_response_navigate(self, client: Stagehand) -> None: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - def test_streaming_response_navigate(self, client: Stagehand) -> None: + def test_streaming_response_navigate(self, client: Browserbase) -> None: with client.sessions.with_streaming_response.navigate( session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", url="https://example.com", @@ -320,7 +320,7 @@ def test_streaming_response_navigate(self, client: Stagehand) -> None: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - def test_path_params_navigate(self, client: Stagehand) -> None: + def test_path_params_navigate(self, client: Browserbase) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `session_id` but received ''"): client.sessions.with_raw_response.navigate( session_id="", @@ -329,7 +329,7 @@ def test_path_params_navigate(self, client: Stagehand) -> None: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - def test_method_observe(self, client: Stagehand) -> None: + def test_method_observe(self, client: Browserbase) -> None: session = client.sessions.observe( session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", ) @@ -337,7 +337,7 @@ def test_method_observe(self, client: Stagehand) -> None: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - def test_method_observe_with_all_params(self, client: Stagehand) -> None: + def test_method_observe_with_all_params(self, client: Browserbase) -> None: session = client.sessions.observe( session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", frame_id="frameId", @@ -358,7 +358,7 @@ def test_method_observe_with_all_params(self, client: Stagehand) -> None: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - def test_raw_response_observe(self, client: Stagehand) -> None: + def test_raw_response_observe(self, client: Browserbase) -> None: response = client.sessions.with_raw_response.observe( session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", ) @@ -370,7 +370,7 @@ def test_raw_response_observe(self, client: Stagehand) -> None: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - def test_streaming_response_observe(self, client: Stagehand) -> None: + def test_streaming_response_observe(self, client: Browserbase) -> None: with client.sessions.with_streaming_response.observe( session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", ) as response: @@ -384,7 +384,7 @@ def test_streaming_response_observe(self, client: Stagehand) -> None: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - def test_path_params_observe(self, client: Stagehand) -> None: + def test_path_params_observe(self, client: Browserbase) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `session_id` but received ''"): client.sessions.with_raw_response.observe( session_id="", @@ -392,7 +392,7 @@ def test_path_params_observe(self, client: Stagehand) -> None: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - def test_method_start(self, client: Stagehand) -> None: + def test_method_start(self, client: Browserbase) -> None: session = client.sessions.start( env="LOCAL", ) @@ -400,7 +400,7 @@ def test_method_start(self, client: Stagehand) -> None: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - def test_method_start_with_all_params(self, client: Stagehand) -> None: + def test_method_start_with_all_params(self, client: Browserbase) -> None: session = client.sessions.start( env="LOCAL", api_key="apiKey", @@ -416,7 +416,7 @@ def test_method_start_with_all_params(self, client: Stagehand) -> None: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - def test_raw_response_start(self, client: Stagehand) -> None: + def test_raw_response_start(self, client: Browserbase) -> None: response = client.sessions.with_raw_response.start( env="LOCAL", ) @@ -428,7 +428,7 @@ def test_raw_response_start(self, client: Stagehand) -> None: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - def test_streaming_response_start(self, client: Stagehand) -> None: + def test_streaming_response_start(self, client: Browserbase) -> None: with client.sessions.with_streaming_response.start( env="LOCAL", ) as response: @@ -448,7 +448,7 @@ class TestAsyncSessions: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - async def test_method_act(self, async_client: AsyncStagehand) -> None: + async def test_method_act(self, async_client: AsyncBrowserbase) -> None: session = await async_client.sessions.act( session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", input="click the sign in button", @@ -457,7 +457,7 @@ async def test_method_act(self, async_client: AsyncStagehand) -> None: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - async def test_method_act_with_all_params(self, async_client: AsyncStagehand) -> None: + async def test_method_act_with_all_params(self, async_client: AsyncBrowserbase) -> None: session = await async_client.sessions.act( session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", input="click the sign in button", @@ -478,7 +478,7 @@ async def test_method_act_with_all_params(self, async_client: AsyncStagehand) -> @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - async def test_raw_response_act(self, async_client: AsyncStagehand) -> None: + async def test_raw_response_act(self, async_client: AsyncBrowserbase) -> None: response = await async_client.sessions.with_raw_response.act( session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", input="click the sign in button", @@ -491,7 +491,7 @@ async def test_raw_response_act(self, async_client: AsyncStagehand) -> None: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - async def test_streaming_response_act(self, async_client: AsyncStagehand) -> None: + async def test_streaming_response_act(self, async_client: AsyncBrowserbase) -> None: async with async_client.sessions.with_streaming_response.act( session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", input="click the sign in button", @@ -506,7 +506,7 @@ async def test_streaming_response_act(self, async_client: AsyncStagehand) -> Non @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - async def test_path_params_act(self, async_client: AsyncStagehand) -> None: + async def test_path_params_act(self, async_client: AsyncBrowserbase) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `session_id` but received ''"): await async_client.sessions.with_raw_response.act( session_id="", @@ -515,7 +515,7 @@ async def test_path_params_act(self, async_client: AsyncStagehand) -> None: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - async def test_method_end(self, async_client: AsyncStagehand) -> None: + async def test_method_end(self, async_client: AsyncBrowserbase) -> None: session = await async_client.sessions.end( "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", ) @@ -523,7 +523,7 @@ async def test_method_end(self, async_client: AsyncStagehand) -> None: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - async def test_raw_response_end(self, async_client: AsyncStagehand) -> None: + async def test_raw_response_end(self, async_client: AsyncBrowserbase) -> None: response = await async_client.sessions.with_raw_response.end( "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", ) @@ -535,7 +535,7 @@ async def test_raw_response_end(self, async_client: AsyncStagehand) -> None: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - async def test_streaming_response_end(self, async_client: AsyncStagehand) -> None: + async def test_streaming_response_end(self, async_client: AsyncBrowserbase) -> None: async with async_client.sessions.with_streaming_response.end( "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", ) as response: @@ -549,7 +549,7 @@ async def test_streaming_response_end(self, async_client: AsyncStagehand) -> Non @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - async def test_path_params_end(self, async_client: AsyncStagehand) -> None: + async def test_path_params_end(self, async_client: AsyncBrowserbase) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `session_id` but received ''"): await async_client.sessions.with_raw_response.end( "", @@ -557,7 +557,7 @@ async def test_path_params_end(self, async_client: AsyncStagehand) -> None: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - async def test_method_execute_agent(self, async_client: AsyncStagehand) -> None: + async def test_method_execute_agent(self, async_client: AsyncBrowserbase) -> None: session = await async_client.sessions.execute_agent( session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", agent_config={}, @@ -567,7 +567,7 @@ async def test_method_execute_agent(self, async_client: AsyncStagehand) -> None: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - async def test_method_execute_agent_with_all_params(self, async_client: AsyncStagehand) -> None: + async def test_method_execute_agent_with_all_params(self, async_client: AsyncBrowserbase) -> None: session = await async_client.sessions.execute_agent( session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", agent_config={ @@ -588,7 +588,7 @@ async def test_method_execute_agent_with_all_params(self, async_client: AsyncSta @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - async def test_raw_response_execute_agent(self, async_client: AsyncStagehand) -> None: + async def test_raw_response_execute_agent(self, async_client: AsyncBrowserbase) -> None: response = await async_client.sessions.with_raw_response.execute_agent( session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", agent_config={}, @@ -602,7 +602,7 @@ async def test_raw_response_execute_agent(self, async_client: AsyncStagehand) -> @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - async def test_streaming_response_execute_agent(self, async_client: AsyncStagehand) -> None: + async def test_streaming_response_execute_agent(self, async_client: AsyncBrowserbase) -> None: async with async_client.sessions.with_streaming_response.execute_agent( session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", agent_config={}, @@ -618,7 +618,7 @@ async def test_streaming_response_execute_agent(self, async_client: AsyncStageha @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - async def test_path_params_execute_agent(self, async_client: AsyncStagehand) -> None: + async def test_path_params_execute_agent(self, async_client: AsyncBrowserbase) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `session_id` but received ''"): await async_client.sessions.with_raw_response.execute_agent( session_id="", @@ -628,7 +628,7 @@ async def test_path_params_execute_agent(self, async_client: AsyncStagehand) -> @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - async def test_method_extract(self, async_client: AsyncStagehand) -> None: + async def test_method_extract(self, async_client: AsyncBrowserbase) -> None: session = await async_client.sessions.extract( session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", ) @@ -636,7 +636,7 @@ async def test_method_extract(self, async_client: AsyncStagehand) -> None: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - async def test_method_extract_with_all_params(self, async_client: AsyncStagehand) -> None: + async def test_method_extract_with_all_params(self, async_client: AsyncBrowserbase) -> None: session = await async_client.sessions.extract( session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", frame_id="frameId", @@ -658,7 +658,7 @@ async def test_method_extract_with_all_params(self, async_client: AsyncStagehand @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - async def test_raw_response_extract(self, async_client: AsyncStagehand) -> None: + async def test_raw_response_extract(self, async_client: AsyncBrowserbase) -> None: response = await async_client.sessions.with_raw_response.extract( session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", ) @@ -670,7 +670,7 @@ async def test_raw_response_extract(self, async_client: AsyncStagehand) -> None: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - async def test_streaming_response_extract(self, async_client: AsyncStagehand) -> None: + async def test_streaming_response_extract(self, async_client: AsyncBrowserbase) -> None: async with async_client.sessions.with_streaming_response.extract( session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", ) as response: @@ -684,7 +684,7 @@ async def test_streaming_response_extract(self, async_client: AsyncStagehand) -> @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - async def test_path_params_extract(self, async_client: AsyncStagehand) -> None: + async def test_path_params_extract(self, async_client: AsyncBrowserbase) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `session_id` but received ''"): await async_client.sessions.with_raw_response.extract( session_id="", @@ -692,7 +692,7 @@ async def test_path_params_extract(self, async_client: AsyncStagehand) -> None: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - async def test_method_navigate(self, async_client: AsyncStagehand) -> None: + async def test_method_navigate(self, async_client: AsyncBrowserbase) -> None: session = await async_client.sessions.navigate( session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", url="https://example.com", @@ -701,7 +701,7 @@ async def test_method_navigate(self, async_client: AsyncStagehand) -> None: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - async def test_method_navigate_with_all_params(self, async_client: AsyncStagehand) -> None: + async def test_method_navigate_with_all_params(self, async_client: AsyncBrowserbase) -> None: session = await async_client.sessions.navigate( session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", url="https://example.com", @@ -713,7 +713,7 @@ async def test_method_navigate_with_all_params(self, async_client: AsyncStagehan @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - async def test_raw_response_navigate(self, async_client: AsyncStagehand) -> None: + async def test_raw_response_navigate(self, async_client: AsyncBrowserbase) -> None: response = await async_client.sessions.with_raw_response.navigate( session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", url="https://example.com", @@ -726,7 +726,7 @@ async def test_raw_response_navigate(self, async_client: AsyncStagehand) -> None @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - async def test_streaming_response_navigate(self, async_client: AsyncStagehand) -> None: + async def test_streaming_response_navigate(self, async_client: AsyncBrowserbase) -> None: async with async_client.sessions.with_streaming_response.navigate( session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", url="https://example.com", @@ -741,7 +741,7 @@ async def test_streaming_response_navigate(self, async_client: AsyncStagehand) - @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - async def test_path_params_navigate(self, async_client: AsyncStagehand) -> None: + async def test_path_params_navigate(self, async_client: AsyncBrowserbase) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `session_id` but received ''"): await async_client.sessions.with_raw_response.navigate( session_id="", @@ -750,7 +750,7 @@ async def test_path_params_navigate(self, async_client: AsyncStagehand) -> None: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - async def test_method_observe(self, async_client: AsyncStagehand) -> None: + async def test_method_observe(self, async_client: AsyncBrowserbase) -> None: session = await async_client.sessions.observe( session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", ) @@ -758,7 +758,7 @@ async def test_method_observe(self, async_client: AsyncStagehand) -> None: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - async def test_method_observe_with_all_params(self, async_client: AsyncStagehand) -> None: + async def test_method_observe_with_all_params(self, async_client: AsyncBrowserbase) -> None: session = await async_client.sessions.observe( session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", frame_id="frameId", @@ -779,7 +779,7 @@ async def test_method_observe_with_all_params(self, async_client: AsyncStagehand @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - async def test_raw_response_observe(self, async_client: AsyncStagehand) -> None: + async def test_raw_response_observe(self, async_client: AsyncBrowserbase) -> None: response = await async_client.sessions.with_raw_response.observe( session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", ) @@ -791,7 +791,7 @@ async def test_raw_response_observe(self, async_client: AsyncStagehand) -> None: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - async def test_streaming_response_observe(self, async_client: AsyncStagehand) -> None: + async def test_streaming_response_observe(self, async_client: AsyncBrowserbase) -> None: async with async_client.sessions.with_streaming_response.observe( session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", ) as response: @@ -805,7 +805,7 @@ async def test_streaming_response_observe(self, async_client: AsyncStagehand) -> @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - async def test_path_params_observe(self, async_client: AsyncStagehand) -> None: + async def test_path_params_observe(self, async_client: AsyncBrowserbase) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `session_id` but received ''"): await async_client.sessions.with_raw_response.observe( session_id="", @@ -813,7 +813,7 @@ async def test_path_params_observe(self, async_client: AsyncStagehand) -> None: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - async def test_method_start(self, async_client: AsyncStagehand) -> None: + async def test_method_start(self, async_client: AsyncBrowserbase) -> None: session = await async_client.sessions.start( env="LOCAL", ) @@ -821,7 +821,7 @@ async def test_method_start(self, async_client: AsyncStagehand) -> None: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - async def test_method_start_with_all_params(self, async_client: AsyncStagehand) -> None: + async def test_method_start_with_all_params(self, async_client: AsyncBrowserbase) -> None: session = await async_client.sessions.start( env="LOCAL", api_key="apiKey", @@ -837,7 +837,7 @@ async def test_method_start_with_all_params(self, async_client: AsyncStagehand) @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - async def test_raw_response_start(self, async_client: AsyncStagehand) -> None: + async def test_raw_response_start(self, async_client: AsyncBrowserbase) -> None: response = await async_client.sessions.with_raw_response.start( env="LOCAL", ) @@ -849,7 +849,7 @@ async def test_raw_response_start(self, async_client: AsyncStagehand) -> None: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - async def test_streaming_response_start(self, async_client: AsyncStagehand) -> None: + async def test_streaming_response_start(self, async_client: AsyncBrowserbase) -> None: async with async_client.sessions.with_streaming_response.start( env="LOCAL", ) as response: diff --git a/tests/conftest.py b/tests/conftest.py index f57b7286..1d6ab407 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -10,7 +10,7 @@ import pytest from pytest_asyncio import is_async_test -from stagehand import Stagehand, AsyncStagehand, DefaultAioHttpClient +from stagehand import Browserbase, AsyncBrowserbase, DefaultAioHttpClient from stagehand._utils import is_dict if TYPE_CHECKING: @@ -49,17 +49,17 @@ def pytest_collection_modifyitems(items: list[pytest.Function]) -> None: @pytest.fixture(scope="session") -def client(request: FixtureRequest) -> Iterator[Stagehand]: +def client(request: FixtureRequest) -> Iterator[Browserbase]: strict = getattr(request, "param", True) if not isinstance(strict, bool): raise TypeError(f"Unexpected fixture parameter type {type(strict)}, expected {bool}") - with Stagehand(base_url=base_url, api_key=api_key, _strict_response_validation=strict) as client: + with Browserbase(base_url=base_url, api_key=api_key, _strict_response_validation=strict) as client: yield client @pytest.fixture(scope="session") -async def async_client(request: FixtureRequest) -> AsyncIterator[AsyncStagehand]: +async def async_client(request: FixtureRequest) -> AsyncIterator[AsyncBrowserbase]: param = getattr(request, "param", True) # defaults @@ -78,7 +78,7 @@ async def async_client(request: FixtureRequest) -> AsyncIterator[AsyncStagehand] else: raise TypeError(f"Unexpected fixture parameter type {type(param)}, expected bool or dict") - async with AsyncStagehand( + async with AsyncBrowserbase( base_url=base_url, api_key=api_key, _strict_response_validation=strict, http_client=http_client ) as client: yield client diff --git a/tests/test_client.py b/tests/test_client.py index b750b99f..b490ea9f 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -18,11 +18,11 @@ from respx import MockRouter from pydantic import ValidationError -from stagehand import Stagehand, AsyncStagehand, APIResponseValidationError +from stagehand import Browserbase, AsyncBrowserbase, APIResponseValidationError from stagehand._types import Omit from stagehand._utils import asyncify from stagehand._models import BaseModel, FinalRequestOptions -from stagehand._exceptions import APIStatusError, APITimeoutError, APIResponseValidationError +from stagehand._exceptions import APIStatusError, APITimeoutError, BrowserbaseError, APIResponseValidationError from stagehand._base_client import ( DEFAULT_TIMEOUT, HTTPX_DEFAULT_TIMEOUT, @@ -50,7 +50,7 @@ def _low_retry_timeout(*_args: Any, **_kwargs: Any) -> float: return 0.1 -def _get_open_connections(client: Stagehand | AsyncStagehand) -> int: +def _get_open_connections(client: Browserbase | AsyncBrowserbase) -> int: transport = client._client._transport assert isinstance(transport, httpx.HTTPTransport) or isinstance(transport, httpx.AsyncHTTPTransport) @@ -58,9 +58,9 @@ def _get_open_connections(client: Stagehand | AsyncStagehand) -> int: return len(pool._requests) -class TestStagehand: +class TestBrowserbase: @pytest.mark.respx(base_url=base_url) - def test_raw_response(self, respx_mock: MockRouter, client: Stagehand) -> None: + def test_raw_response(self, respx_mock: MockRouter, client: Browserbase) -> None: respx_mock.post("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"})) response = client.post("/foo", cast_to=httpx.Response) @@ -69,7 +69,7 @@ def test_raw_response(self, respx_mock: MockRouter, client: Stagehand) -> None: assert response.json() == {"foo": "bar"} @pytest.mark.respx(base_url=base_url) - def test_raw_response_for_binary(self, respx_mock: MockRouter, client: Stagehand) -> None: + def test_raw_response_for_binary(self, respx_mock: MockRouter, client: Browserbase) -> None: respx_mock.post("/foo").mock( return_value=httpx.Response(200, headers={"Content-Type": "application/binary"}, content='{"foo": "bar"}') ) @@ -79,7 +79,7 @@ def test_raw_response_for_binary(self, respx_mock: MockRouter, client: Stagehand assert isinstance(response, httpx.Response) assert response.json() == {"foo": "bar"} - def test_copy(self, client: Stagehand) -> None: + def test_copy(self, client: Browserbase) -> None: copied = client.copy() assert id(copied) != id(client) @@ -87,7 +87,7 @@ def test_copy(self, client: Stagehand) -> None: assert copied.api_key == "another My API Key" assert client.api_key == "My API Key" - def test_copy_default_options(self, client: Stagehand) -> None: + def test_copy_default_options(self, client: Browserbase) -> None: # options that have a default are overridden correctly copied = client.copy(max_retries=7) assert copied.max_retries == 7 @@ -104,7 +104,7 @@ def test_copy_default_options(self, client: Stagehand) -> None: assert isinstance(client.timeout, httpx.Timeout) def test_copy_default_headers(self) -> None: - client = Stagehand( + client = Browserbase( base_url=base_url, api_key=api_key, _strict_response_validation=True, default_headers={"X-Foo": "bar"} ) assert client.default_headers["X-Foo"] == "bar" @@ -139,7 +139,7 @@ def test_copy_default_headers(self) -> None: client.close() def test_copy_default_query(self) -> None: - client = Stagehand( + client = Browserbase( base_url=base_url, api_key=api_key, _strict_response_validation=True, default_query={"foo": "bar"} ) assert _get_params(client)["foo"] == "bar" @@ -176,7 +176,7 @@ def test_copy_default_query(self) -> None: client.close() - def test_copy_signature(self, client: Stagehand) -> None: + def test_copy_signature(self, client: Browserbase) -> None: # ensure the same parameters that can be passed to the client are defined in the `.copy()` method init_signature = inspect.signature( # mypy doesn't like that we access the `__init__` property. @@ -193,7 +193,7 @@ def test_copy_signature(self, client: Stagehand) -> None: assert copy_param is not None, f"copy() signature is missing the {name} param" @pytest.mark.skipif(sys.version_info >= (3, 10), reason="fails because of a memory leak that started from 3.12") - def test_copy_build_request(self, client: Stagehand) -> None: + def test_copy_build_request(self, client: Browserbase) -> None: options = FinalRequestOptions(method="get", url="/foo") def build_request(options: FinalRequestOptions) -> None: @@ -255,7 +255,7 @@ def add_leak(leaks: list[tracemalloc.StatisticDiff], diff: tracemalloc.Statistic print(frame) raise AssertionError() - def test_request_timeout(self, client: Stagehand) -> None: + def test_request_timeout(self, client: Browserbase) -> None: request = client._build_request(FinalRequestOptions(method="get", url="/foo")) timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore assert timeout == DEFAULT_TIMEOUT @@ -265,7 +265,7 @@ def test_request_timeout(self, client: Stagehand) -> None: assert timeout == httpx.Timeout(100.0) def test_client_timeout_option(self) -> None: - client = Stagehand( + client = Browserbase( base_url=base_url, api_key=api_key, _strict_response_validation=True, timeout=httpx.Timeout(0) ) @@ -278,7 +278,7 @@ def test_client_timeout_option(self) -> None: def test_http_client_timeout_option(self) -> None: # custom timeout given to the httpx client should be used with httpx.Client(timeout=None) as http_client: - client = Stagehand( + client = Browserbase( base_url=base_url, api_key=api_key, _strict_response_validation=True, http_client=http_client ) @@ -290,7 +290,7 @@ def test_http_client_timeout_option(self) -> None: # no timeout given to the httpx client should not use the httpx default with httpx.Client() as http_client: - client = Stagehand( + client = Browserbase( base_url=base_url, api_key=api_key, _strict_response_validation=True, http_client=http_client ) @@ -302,7 +302,7 @@ def test_http_client_timeout_option(self) -> None: # explicitly passing the default timeout currently results in it being ignored with httpx.Client(timeout=HTTPX_DEFAULT_TIMEOUT) as http_client: - client = Stagehand( + client = Browserbase( base_url=base_url, api_key=api_key, _strict_response_validation=True, http_client=http_client ) @@ -315,7 +315,7 @@ def test_http_client_timeout_option(self) -> None: async def test_invalid_http_client(self) -> None: with pytest.raises(TypeError, match="Invalid `http_client` arg"): async with httpx.AsyncClient() as http_client: - Stagehand( + Browserbase( base_url=base_url, api_key=api_key, _strict_response_validation=True, @@ -323,14 +323,14 @@ async def test_invalid_http_client(self) -> None: ) def test_default_headers_option(self) -> None: - test_client = Stagehand( + test_client = Browserbase( base_url=base_url, api_key=api_key, _strict_response_validation=True, default_headers={"X-Foo": "bar"} ) request = test_client._build_request(FinalRequestOptions(method="get", url="/foo")) assert request.headers.get("x-foo") == "bar" assert request.headers.get("x-stainless-lang") == "python" - test_client2 = Stagehand( + test_client2 = Browserbase( base_url=base_url, api_key=api_key, _strict_response_validation=True, @@ -347,26 +347,17 @@ def test_default_headers_option(self) -> None: test_client2.close() def test_validate_headers(self) -> None: - client = Stagehand(base_url=base_url, api_key=api_key, _strict_response_validation=True) + client = Browserbase(base_url=base_url, api_key=api_key, _strict_response_validation=True) request = client._build_request(FinalRequestOptions(method="get", url="/foo")) assert request.headers.get("Authorization") == f"Bearer {api_key}" - with update_env(**{"STAGEHAND_API_KEY": Omit()}): - client2 = Stagehand(base_url=base_url, api_key=None, _strict_response_validation=True) - - with pytest.raises( - TypeError, - match="Could not resolve authentication method. Expected the api_key to be set. Or for the `Authorization` headers to be explicitly omitted", - ): - client2._build_request(FinalRequestOptions(method="get", url="/foo")) - - request2 = client2._build_request( - FinalRequestOptions(method="get", url="/foo", headers={"Authorization": Omit()}) - ) - assert request2.headers.get("Authorization") is None + with pytest.raises(BrowserbaseError): + with update_env(**{"STAGEHAND_API_KEY": Omit()}): + client2 = Browserbase(base_url=base_url, api_key=None, _strict_response_validation=True) + _ = client2 def test_default_query_option(self) -> None: - client = Stagehand( + client = Browserbase( base_url=base_url, api_key=api_key, _strict_response_validation=True, default_query={"query_param": "bar"} ) request = client._build_request(FinalRequestOptions(method="get", url="/foo")) @@ -385,7 +376,7 @@ def test_default_query_option(self) -> None: client.close() - def test_request_extra_json(self, client: Stagehand) -> None: + def test_request_extra_json(self, client: Browserbase) -> None: request = client._build_request( FinalRequestOptions( method="post", @@ -419,7 +410,7 @@ def test_request_extra_json(self, client: Stagehand) -> None: data = json.loads(request.content.decode("utf-8")) assert data == {"foo": "bar", "baz": None} - def test_request_extra_headers(self, client: Stagehand) -> None: + def test_request_extra_headers(self, client: Browserbase) -> None: request = client._build_request( FinalRequestOptions( method="post", @@ -441,7 +432,7 @@ def test_request_extra_headers(self, client: Stagehand) -> None: ) assert request.headers.get("X-Bar") == "false" - def test_request_extra_query(self, client: Stagehand) -> None: + def test_request_extra_query(self, client: Browserbase) -> None: request = client._build_request( FinalRequestOptions( method="post", @@ -482,7 +473,7 @@ def test_request_extra_query(self, client: Stagehand) -> None: params = dict(request.url.params) assert params == {"foo": "2"} - def test_multipart_repeating_array(self, client: Stagehand) -> None: + def test_multipart_repeating_array(self, client: Browserbase) -> None: request = client._build_request( FinalRequestOptions.construct( method="post", @@ -512,7 +503,7 @@ def test_multipart_repeating_array(self, client: Stagehand) -> None: ] @pytest.mark.respx(base_url=base_url) - def test_basic_union_response(self, respx_mock: MockRouter, client: Stagehand) -> None: + def test_basic_union_response(self, respx_mock: MockRouter, client: Browserbase) -> None: class Model1(BaseModel): name: str @@ -526,7 +517,7 @@ class Model2(BaseModel): assert response.foo == "bar" @pytest.mark.respx(base_url=base_url) - def test_union_response_different_types(self, respx_mock: MockRouter, client: Stagehand) -> None: + def test_union_response_different_types(self, respx_mock: MockRouter, client: Browserbase) -> None: """Union of objects with the same field name using a different type""" class Model1(BaseModel): @@ -548,7 +539,7 @@ class Model2(BaseModel): assert response.foo == 1 @pytest.mark.respx(base_url=base_url) - def test_non_application_json_content_type_for_json_data(self, respx_mock: MockRouter, client: Stagehand) -> None: + def test_non_application_json_content_type_for_json_data(self, respx_mock: MockRouter, client: Browserbase) -> None: """ Response that sets Content-Type to something other than application/json but returns json data """ @@ -569,7 +560,9 @@ class Model(BaseModel): assert response.foo == 2 def test_base_url_setter(self) -> None: - client = Stagehand(base_url="https://example.com/from_init", api_key=api_key, _strict_response_validation=True) + client = Browserbase( + base_url="https://example.com/from_init", api_key=api_key, _strict_response_validation=True + ) assert client.base_url == "https://example.com/from_init/" client.base_url = "https://example.com/from_setter" # type: ignore[assignment] @@ -579,27 +572,29 @@ def test_base_url_setter(self) -> None: client.close() def test_base_url_env(self) -> None: - with update_env(STAGEHAND_BASE_URL="http://localhost:5000/from/env"): - client = Stagehand(api_key=api_key, _strict_response_validation=True) + with update_env(BROWSERBASE_BASE_URL="http://localhost:5000/from/env"): + client = Browserbase(api_key=api_key, _strict_response_validation=True) assert client.base_url == "http://localhost:5000/from/env/" # explicit environment arg requires explicitness - with update_env(STAGEHAND_BASE_URL="http://localhost:5000/from/env"): + with update_env(BROWSERBASE_BASE_URL="http://localhost:5000/from/env"): with pytest.raises(ValueError, match=r"you must pass base_url=None"): - Stagehand(api_key=api_key, _strict_response_validation=True, environment="production") + Browserbase(api_key=api_key, _strict_response_validation=True, environment="production") - client = Stagehand( + client = Browserbase( base_url=None, api_key=api_key, _strict_response_validation=True, environment="production" ) - assert str(client.base_url).startswith("http://localhost:3000/v1") + assert str(client.base_url).startswith("https://api.stagehand.browserbase.com/v1") client.close() @pytest.mark.parametrize( "client", [ - Stagehand(base_url="http://localhost:5000/custom/path/", api_key=api_key, _strict_response_validation=True), - Stagehand( + Browserbase( + base_url="http://localhost:5000/custom/path/", api_key=api_key, _strict_response_validation=True + ), + Browserbase( base_url="http://localhost:5000/custom/path/", api_key=api_key, _strict_response_validation=True, @@ -608,7 +603,7 @@ def test_base_url_env(self) -> None: ], ids=["standard", "custom http client"], ) - def test_base_url_trailing_slash(self, client: Stagehand) -> None: + def test_base_url_trailing_slash(self, client: Browserbase) -> None: request = client._build_request( FinalRequestOptions( method="post", @@ -622,8 +617,10 @@ def test_base_url_trailing_slash(self, client: Stagehand) -> None: @pytest.mark.parametrize( "client", [ - Stagehand(base_url="http://localhost:5000/custom/path/", api_key=api_key, _strict_response_validation=True), - Stagehand( + Browserbase( + base_url="http://localhost:5000/custom/path/", api_key=api_key, _strict_response_validation=True + ), + Browserbase( base_url="http://localhost:5000/custom/path/", api_key=api_key, _strict_response_validation=True, @@ -632,7 +629,7 @@ def test_base_url_trailing_slash(self, client: Stagehand) -> None: ], ids=["standard", "custom http client"], ) - def test_base_url_no_trailing_slash(self, client: Stagehand) -> None: + def test_base_url_no_trailing_slash(self, client: Browserbase) -> None: request = client._build_request( FinalRequestOptions( method="post", @@ -646,8 +643,10 @@ def test_base_url_no_trailing_slash(self, client: Stagehand) -> None: @pytest.mark.parametrize( "client", [ - Stagehand(base_url="http://localhost:5000/custom/path/", api_key=api_key, _strict_response_validation=True), - Stagehand( + Browserbase( + base_url="http://localhost:5000/custom/path/", api_key=api_key, _strict_response_validation=True + ), + Browserbase( base_url="http://localhost:5000/custom/path/", api_key=api_key, _strict_response_validation=True, @@ -656,7 +655,7 @@ def test_base_url_no_trailing_slash(self, client: Stagehand) -> None: ], ids=["standard", "custom http client"], ) - def test_absolute_request_url(self, client: Stagehand) -> None: + def test_absolute_request_url(self, client: Browserbase) -> None: request = client._build_request( FinalRequestOptions( method="post", @@ -668,7 +667,7 @@ def test_absolute_request_url(self, client: Stagehand) -> None: client.close() def test_copied_client_does_not_close_http(self) -> None: - test_client = Stagehand(base_url=base_url, api_key=api_key, _strict_response_validation=True) + test_client = Browserbase(base_url=base_url, api_key=api_key, _strict_response_validation=True) assert not test_client.is_closed() copied = test_client.copy() @@ -679,7 +678,7 @@ def test_copied_client_does_not_close_http(self) -> None: assert not test_client.is_closed() def test_client_context_manager(self) -> None: - test_client = Stagehand(base_url=base_url, api_key=api_key, _strict_response_validation=True) + test_client = Browserbase(base_url=base_url, api_key=api_key, _strict_response_validation=True) with test_client as c2: assert c2 is test_client assert not c2.is_closed() @@ -687,7 +686,7 @@ def test_client_context_manager(self) -> None: assert test_client.is_closed() @pytest.mark.respx(base_url=base_url) - def test_client_response_validation_error(self, respx_mock: MockRouter, client: Stagehand) -> None: + def test_client_response_validation_error(self, respx_mock: MockRouter, client: Browserbase) -> None: class Model(BaseModel): foo: str @@ -700,7 +699,9 @@ class Model(BaseModel): def test_client_max_retries_validation(self) -> None: with pytest.raises(TypeError, match=r"max_retries cannot be None"): - Stagehand(base_url=base_url, api_key=api_key, _strict_response_validation=True, max_retries=cast(Any, None)) + Browserbase( + base_url=base_url, api_key=api_key, _strict_response_validation=True, max_retries=cast(Any, None) + ) @pytest.mark.respx(base_url=base_url) def test_received_text_for_expected_json(self, respx_mock: MockRouter) -> None: @@ -709,12 +710,12 @@ class Model(BaseModel): respx_mock.get("/foo").mock(return_value=httpx.Response(200, text="my-custom-format")) - strict_client = Stagehand(base_url=base_url, api_key=api_key, _strict_response_validation=True) + strict_client = Browserbase(base_url=base_url, api_key=api_key, _strict_response_validation=True) with pytest.raises(APIResponseValidationError): strict_client.get("/foo", cast_to=Model) - non_strict_client = Stagehand(base_url=base_url, api_key=api_key, _strict_response_validation=False) + non_strict_client = Browserbase(base_url=base_url, api_key=api_key, _strict_response_validation=False) response = non_strict_client.get("/foo", cast_to=Model) assert isinstance(response, str) # type: ignore[unreachable] @@ -745,7 +746,7 @@ class Model(BaseModel): ) @mock.patch("time.time", mock.MagicMock(return_value=1696004797)) def test_parse_retry_after_header( - self, remaining_retries: int, retry_after: str, timeout: float, client: Stagehand + self, remaining_retries: int, retry_after: str, timeout: float, client: Browserbase ) -> None: headers = httpx.Headers({"retry-after": retry_after}) options = FinalRequestOptions(method="get", url="/foo", max_retries=3) @@ -754,7 +755,7 @@ def test_parse_retry_after_header( @mock.patch("stagehand._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) @pytest.mark.respx(base_url=base_url) - def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter, client: Stagehand) -> None: + def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter, client: Browserbase) -> None: respx_mock.post("/sessions/start").mock(side_effect=httpx.TimeoutException("Test timeout error")) with pytest.raises(APITimeoutError): @@ -764,7 +765,7 @@ def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter, clien @mock.patch("stagehand._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) @pytest.mark.respx(base_url=base_url) - def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter, client: Stagehand) -> None: + def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter, client: Browserbase) -> None: respx_mock.post("/sessions/start").mock(return_value=httpx.Response(500)) with pytest.raises(APIStatusError): @@ -777,7 +778,7 @@ def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter, client @pytest.mark.parametrize("failure_mode", ["status", "exception"]) def test_retries_taken( self, - client: Stagehand, + client: Browserbase, failures_before_success: int, failure_mode: Literal["status", "exception"], respx_mock: MockRouter, @@ -806,7 +807,7 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: @mock.patch("stagehand._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) @pytest.mark.respx(base_url=base_url) def test_omit_retry_count_header( - self, client: Stagehand, failures_before_success: int, respx_mock: MockRouter + self, client: Browserbase, failures_before_success: int, respx_mock: MockRouter ) -> None: client = client.with_options(max_retries=4) @@ -831,7 +832,7 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: @mock.patch("stagehand._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) @pytest.mark.respx(base_url=base_url) def test_overwrite_retry_count_header( - self, client: Stagehand, failures_before_success: int, respx_mock: MockRouter + self, client: Browserbase, failures_before_success: int, respx_mock: MockRouter ) -> None: client = client.with_options(max_retries=4) @@ -873,7 +874,7 @@ def test_default_client_creation(self) -> None: ) @pytest.mark.respx(base_url=base_url) - def test_follow_redirects(self, respx_mock: MockRouter, client: Stagehand) -> None: + def test_follow_redirects(self, respx_mock: MockRouter, client: Browserbase) -> None: # Test that the default follow_redirects=True allows following redirects respx_mock.post("/redirect").mock( return_value=httpx.Response(302, headers={"Location": f"{base_url}/redirected"}) @@ -885,7 +886,7 @@ def test_follow_redirects(self, respx_mock: MockRouter, client: Stagehand) -> No assert response.json() == {"status": "ok"} @pytest.mark.respx(base_url=base_url) - def test_follow_redirects_disabled(self, respx_mock: MockRouter, client: Stagehand) -> None: + def test_follow_redirects_disabled(self, respx_mock: MockRouter, client: Browserbase) -> None: # Test that follow_redirects=False prevents following redirects respx_mock.post("/redirect").mock( return_value=httpx.Response(302, headers={"Location": f"{base_url}/redirected"}) @@ -898,9 +899,9 @@ def test_follow_redirects_disabled(self, respx_mock: MockRouter, client: Stageha assert exc_info.value.response.headers["Location"] == f"{base_url}/redirected" -class TestAsyncStagehand: +class TestAsyncBrowserbase: @pytest.mark.respx(base_url=base_url) - async def test_raw_response(self, respx_mock: MockRouter, async_client: AsyncStagehand) -> None: + async def test_raw_response(self, respx_mock: MockRouter, async_client: AsyncBrowserbase) -> None: respx_mock.post("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"})) response = await async_client.post("/foo", cast_to=httpx.Response) @@ -909,7 +910,7 @@ async def test_raw_response(self, respx_mock: MockRouter, async_client: AsyncSta assert response.json() == {"foo": "bar"} @pytest.mark.respx(base_url=base_url) - async def test_raw_response_for_binary(self, respx_mock: MockRouter, async_client: AsyncStagehand) -> None: + async def test_raw_response_for_binary(self, respx_mock: MockRouter, async_client: AsyncBrowserbase) -> None: respx_mock.post("/foo").mock( return_value=httpx.Response(200, headers={"Content-Type": "application/binary"}, content='{"foo": "bar"}') ) @@ -919,7 +920,7 @@ async def test_raw_response_for_binary(self, respx_mock: MockRouter, async_clien assert isinstance(response, httpx.Response) assert response.json() == {"foo": "bar"} - def test_copy(self, async_client: AsyncStagehand) -> None: + def test_copy(self, async_client: AsyncBrowserbase) -> None: copied = async_client.copy() assert id(copied) != id(async_client) @@ -927,7 +928,7 @@ def test_copy(self, async_client: AsyncStagehand) -> None: assert copied.api_key == "another My API Key" assert async_client.api_key == "My API Key" - def test_copy_default_options(self, async_client: AsyncStagehand) -> None: + def test_copy_default_options(self, async_client: AsyncBrowserbase) -> None: # options that have a default are overridden correctly copied = async_client.copy(max_retries=7) assert copied.max_retries == 7 @@ -944,7 +945,7 @@ def test_copy_default_options(self, async_client: AsyncStagehand) -> None: assert isinstance(async_client.timeout, httpx.Timeout) async def test_copy_default_headers(self) -> None: - client = AsyncStagehand( + client = AsyncBrowserbase( base_url=base_url, api_key=api_key, _strict_response_validation=True, default_headers={"X-Foo": "bar"} ) assert client.default_headers["X-Foo"] == "bar" @@ -979,7 +980,7 @@ async def test_copy_default_headers(self) -> None: await client.close() async def test_copy_default_query(self) -> None: - client = AsyncStagehand( + client = AsyncBrowserbase( base_url=base_url, api_key=api_key, _strict_response_validation=True, default_query={"foo": "bar"} ) assert _get_params(client)["foo"] == "bar" @@ -1016,7 +1017,7 @@ async def test_copy_default_query(self) -> None: await client.close() - def test_copy_signature(self, async_client: AsyncStagehand) -> None: + def test_copy_signature(self, async_client: AsyncBrowserbase) -> None: # ensure the same parameters that can be passed to the client are defined in the `.copy()` method init_signature = inspect.signature( # mypy doesn't like that we access the `__init__` property. @@ -1033,7 +1034,7 @@ def test_copy_signature(self, async_client: AsyncStagehand) -> None: assert copy_param is not None, f"copy() signature is missing the {name} param" @pytest.mark.skipif(sys.version_info >= (3, 10), reason="fails because of a memory leak that started from 3.12") - def test_copy_build_request(self, async_client: AsyncStagehand) -> None: + def test_copy_build_request(self, async_client: AsyncBrowserbase) -> None: options = FinalRequestOptions(method="get", url="/foo") def build_request(options: FinalRequestOptions) -> None: @@ -1095,7 +1096,7 @@ def add_leak(leaks: list[tracemalloc.StatisticDiff], diff: tracemalloc.Statistic print(frame) raise AssertionError() - async def test_request_timeout(self, async_client: AsyncStagehand) -> None: + async def test_request_timeout(self, async_client: AsyncBrowserbase) -> None: request = async_client._build_request(FinalRequestOptions(method="get", url="/foo")) timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore assert timeout == DEFAULT_TIMEOUT @@ -1107,7 +1108,7 @@ async def test_request_timeout(self, async_client: AsyncStagehand) -> None: assert timeout == httpx.Timeout(100.0) async def test_client_timeout_option(self) -> None: - client = AsyncStagehand( + client = AsyncBrowserbase( base_url=base_url, api_key=api_key, _strict_response_validation=True, timeout=httpx.Timeout(0) ) @@ -1120,7 +1121,7 @@ async def test_client_timeout_option(self) -> None: async def test_http_client_timeout_option(self) -> None: # custom timeout given to the httpx client should be used async with httpx.AsyncClient(timeout=None) as http_client: - client = AsyncStagehand( + client = AsyncBrowserbase( base_url=base_url, api_key=api_key, _strict_response_validation=True, http_client=http_client ) @@ -1132,7 +1133,7 @@ async def test_http_client_timeout_option(self) -> None: # no timeout given to the httpx client should not use the httpx default async with httpx.AsyncClient() as http_client: - client = AsyncStagehand( + client = AsyncBrowserbase( base_url=base_url, api_key=api_key, _strict_response_validation=True, http_client=http_client ) @@ -1144,7 +1145,7 @@ async def test_http_client_timeout_option(self) -> None: # explicitly passing the default timeout currently results in it being ignored async with httpx.AsyncClient(timeout=HTTPX_DEFAULT_TIMEOUT) as http_client: - client = AsyncStagehand( + client = AsyncBrowserbase( base_url=base_url, api_key=api_key, _strict_response_validation=True, http_client=http_client ) @@ -1157,7 +1158,7 @@ async def test_http_client_timeout_option(self) -> None: def test_invalid_http_client(self) -> None: with pytest.raises(TypeError, match="Invalid `http_client` arg"): with httpx.Client() as http_client: - AsyncStagehand( + AsyncBrowserbase( base_url=base_url, api_key=api_key, _strict_response_validation=True, @@ -1165,14 +1166,14 @@ def test_invalid_http_client(self) -> None: ) async def test_default_headers_option(self) -> None: - test_client = AsyncStagehand( + test_client = AsyncBrowserbase( base_url=base_url, api_key=api_key, _strict_response_validation=True, default_headers={"X-Foo": "bar"} ) request = test_client._build_request(FinalRequestOptions(method="get", url="/foo")) assert request.headers.get("x-foo") == "bar" assert request.headers.get("x-stainless-lang") == "python" - test_client2 = AsyncStagehand( + test_client2 = AsyncBrowserbase( base_url=base_url, api_key=api_key, _strict_response_validation=True, @@ -1189,26 +1190,17 @@ async def test_default_headers_option(self) -> None: await test_client2.close() def test_validate_headers(self) -> None: - client = AsyncStagehand(base_url=base_url, api_key=api_key, _strict_response_validation=True) + client = AsyncBrowserbase(base_url=base_url, api_key=api_key, _strict_response_validation=True) request = client._build_request(FinalRequestOptions(method="get", url="/foo")) assert request.headers.get("Authorization") == f"Bearer {api_key}" - with update_env(**{"STAGEHAND_API_KEY": Omit()}): - client2 = AsyncStagehand(base_url=base_url, api_key=None, _strict_response_validation=True) - - with pytest.raises( - TypeError, - match="Could not resolve authentication method. Expected the api_key to be set. Or for the `Authorization` headers to be explicitly omitted", - ): - client2._build_request(FinalRequestOptions(method="get", url="/foo")) - - request2 = client2._build_request( - FinalRequestOptions(method="get", url="/foo", headers={"Authorization": Omit()}) - ) - assert request2.headers.get("Authorization") is None + with pytest.raises(BrowserbaseError): + with update_env(**{"STAGEHAND_API_KEY": Omit()}): + client2 = AsyncBrowserbase(base_url=base_url, api_key=None, _strict_response_validation=True) + _ = client2 async def test_default_query_option(self) -> None: - client = AsyncStagehand( + client = AsyncBrowserbase( base_url=base_url, api_key=api_key, _strict_response_validation=True, default_query={"query_param": "bar"} ) request = client._build_request(FinalRequestOptions(method="get", url="/foo")) @@ -1227,7 +1219,7 @@ async def test_default_query_option(self) -> None: await client.close() - def test_request_extra_json(self, client: Stagehand) -> None: + def test_request_extra_json(self, client: Browserbase) -> None: request = client._build_request( FinalRequestOptions( method="post", @@ -1261,7 +1253,7 @@ def test_request_extra_json(self, client: Stagehand) -> None: data = json.loads(request.content.decode("utf-8")) assert data == {"foo": "bar", "baz": None} - def test_request_extra_headers(self, client: Stagehand) -> None: + def test_request_extra_headers(self, client: Browserbase) -> None: request = client._build_request( FinalRequestOptions( method="post", @@ -1283,7 +1275,7 @@ def test_request_extra_headers(self, client: Stagehand) -> None: ) assert request.headers.get("X-Bar") == "false" - def test_request_extra_query(self, client: Stagehand) -> None: + def test_request_extra_query(self, client: Browserbase) -> None: request = client._build_request( FinalRequestOptions( method="post", @@ -1324,7 +1316,7 @@ def test_request_extra_query(self, client: Stagehand) -> None: params = dict(request.url.params) assert params == {"foo": "2"} - def test_multipart_repeating_array(self, async_client: AsyncStagehand) -> None: + def test_multipart_repeating_array(self, async_client: AsyncBrowserbase) -> None: request = async_client._build_request( FinalRequestOptions.construct( method="post", @@ -1354,7 +1346,7 @@ def test_multipart_repeating_array(self, async_client: AsyncStagehand) -> None: ] @pytest.mark.respx(base_url=base_url) - async def test_basic_union_response(self, respx_mock: MockRouter, async_client: AsyncStagehand) -> None: + async def test_basic_union_response(self, respx_mock: MockRouter, async_client: AsyncBrowserbase) -> None: class Model1(BaseModel): name: str @@ -1368,7 +1360,7 @@ class Model2(BaseModel): assert response.foo == "bar" @pytest.mark.respx(base_url=base_url) - async def test_union_response_different_types(self, respx_mock: MockRouter, async_client: AsyncStagehand) -> None: + async def test_union_response_different_types(self, respx_mock: MockRouter, async_client: AsyncBrowserbase) -> None: """Union of objects with the same field name using a different type""" class Model1(BaseModel): @@ -1391,7 +1383,7 @@ class Model2(BaseModel): @pytest.mark.respx(base_url=base_url) async def test_non_application_json_content_type_for_json_data( - self, respx_mock: MockRouter, async_client: AsyncStagehand + self, respx_mock: MockRouter, async_client: AsyncBrowserbase ) -> None: """ Response that sets Content-Type to something other than application/json but returns json data @@ -1413,7 +1405,7 @@ class Model(BaseModel): assert response.foo == 2 async def test_base_url_setter(self) -> None: - client = AsyncStagehand( + client = AsyncBrowserbase( base_url="https://example.com/from_init", api_key=api_key, _strict_response_validation=True ) assert client.base_url == "https://example.com/from_init/" @@ -1425,29 +1417,29 @@ async def test_base_url_setter(self) -> None: await client.close() async def test_base_url_env(self) -> None: - with update_env(STAGEHAND_BASE_URL="http://localhost:5000/from/env"): - client = AsyncStagehand(api_key=api_key, _strict_response_validation=True) + with update_env(BROWSERBASE_BASE_URL="http://localhost:5000/from/env"): + client = AsyncBrowserbase(api_key=api_key, _strict_response_validation=True) assert client.base_url == "http://localhost:5000/from/env/" # explicit environment arg requires explicitness - with update_env(STAGEHAND_BASE_URL="http://localhost:5000/from/env"): + with update_env(BROWSERBASE_BASE_URL="http://localhost:5000/from/env"): with pytest.raises(ValueError, match=r"you must pass base_url=None"): - AsyncStagehand(api_key=api_key, _strict_response_validation=True, environment="production") + AsyncBrowserbase(api_key=api_key, _strict_response_validation=True, environment="production") - client = AsyncStagehand( + client = AsyncBrowserbase( base_url=None, api_key=api_key, _strict_response_validation=True, environment="production" ) - assert str(client.base_url).startswith("http://localhost:3000/v1") + assert str(client.base_url).startswith("https://api.stagehand.browserbase.com/v1") await client.close() @pytest.mark.parametrize( "client", [ - AsyncStagehand( + AsyncBrowserbase( base_url="http://localhost:5000/custom/path/", api_key=api_key, _strict_response_validation=True ), - AsyncStagehand( + AsyncBrowserbase( base_url="http://localhost:5000/custom/path/", api_key=api_key, _strict_response_validation=True, @@ -1456,7 +1448,7 @@ async def test_base_url_env(self) -> None: ], ids=["standard", "custom http client"], ) - async def test_base_url_trailing_slash(self, client: AsyncStagehand) -> None: + async def test_base_url_trailing_slash(self, client: AsyncBrowserbase) -> None: request = client._build_request( FinalRequestOptions( method="post", @@ -1470,10 +1462,10 @@ async def test_base_url_trailing_slash(self, client: AsyncStagehand) -> None: @pytest.mark.parametrize( "client", [ - AsyncStagehand( + AsyncBrowserbase( base_url="http://localhost:5000/custom/path/", api_key=api_key, _strict_response_validation=True ), - AsyncStagehand( + AsyncBrowserbase( base_url="http://localhost:5000/custom/path/", api_key=api_key, _strict_response_validation=True, @@ -1482,7 +1474,7 @@ async def test_base_url_trailing_slash(self, client: AsyncStagehand) -> None: ], ids=["standard", "custom http client"], ) - async def test_base_url_no_trailing_slash(self, client: AsyncStagehand) -> None: + async def test_base_url_no_trailing_slash(self, client: AsyncBrowserbase) -> None: request = client._build_request( FinalRequestOptions( method="post", @@ -1496,10 +1488,10 @@ async def test_base_url_no_trailing_slash(self, client: AsyncStagehand) -> None: @pytest.mark.parametrize( "client", [ - AsyncStagehand( + AsyncBrowserbase( base_url="http://localhost:5000/custom/path/", api_key=api_key, _strict_response_validation=True ), - AsyncStagehand( + AsyncBrowserbase( base_url="http://localhost:5000/custom/path/", api_key=api_key, _strict_response_validation=True, @@ -1508,7 +1500,7 @@ async def test_base_url_no_trailing_slash(self, client: AsyncStagehand) -> None: ], ids=["standard", "custom http client"], ) - async def test_absolute_request_url(self, client: AsyncStagehand) -> None: + async def test_absolute_request_url(self, client: AsyncBrowserbase) -> None: request = client._build_request( FinalRequestOptions( method="post", @@ -1520,7 +1512,7 @@ async def test_absolute_request_url(self, client: AsyncStagehand) -> None: await client.close() async def test_copied_client_does_not_close_http(self) -> None: - test_client = AsyncStagehand(base_url=base_url, api_key=api_key, _strict_response_validation=True) + test_client = AsyncBrowserbase(base_url=base_url, api_key=api_key, _strict_response_validation=True) assert not test_client.is_closed() copied = test_client.copy() @@ -1532,7 +1524,7 @@ async def test_copied_client_does_not_close_http(self) -> None: assert not test_client.is_closed() async def test_client_context_manager(self) -> None: - test_client = AsyncStagehand(base_url=base_url, api_key=api_key, _strict_response_validation=True) + test_client = AsyncBrowserbase(base_url=base_url, api_key=api_key, _strict_response_validation=True) async with test_client as c2: assert c2 is test_client assert not c2.is_closed() @@ -1540,7 +1532,9 @@ async def test_client_context_manager(self) -> None: assert test_client.is_closed() @pytest.mark.respx(base_url=base_url) - async def test_client_response_validation_error(self, respx_mock: MockRouter, async_client: AsyncStagehand) -> None: + async def test_client_response_validation_error( + self, respx_mock: MockRouter, async_client: AsyncBrowserbase + ) -> None: class Model(BaseModel): foo: str @@ -1553,7 +1547,7 @@ class Model(BaseModel): async def test_client_max_retries_validation(self) -> None: with pytest.raises(TypeError, match=r"max_retries cannot be None"): - AsyncStagehand( + AsyncBrowserbase( base_url=base_url, api_key=api_key, _strict_response_validation=True, max_retries=cast(Any, None) ) @@ -1564,12 +1558,12 @@ class Model(BaseModel): respx_mock.get("/foo").mock(return_value=httpx.Response(200, text="my-custom-format")) - strict_client = AsyncStagehand(base_url=base_url, api_key=api_key, _strict_response_validation=True) + strict_client = AsyncBrowserbase(base_url=base_url, api_key=api_key, _strict_response_validation=True) with pytest.raises(APIResponseValidationError): await strict_client.get("/foo", cast_to=Model) - non_strict_client = AsyncStagehand(base_url=base_url, api_key=api_key, _strict_response_validation=False) + non_strict_client = AsyncBrowserbase(base_url=base_url, api_key=api_key, _strict_response_validation=False) response = await non_strict_client.get("/foo", cast_to=Model) assert isinstance(response, str) # type: ignore[unreachable] @@ -1600,7 +1594,7 @@ class Model(BaseModel): ) @mock.patch("time.time", mock.MagicMock(return_value=1696004797)) async def test_parse_retry_after_header( - self, remaining_retries: int, retry_after: str, timeout: float, async_client: AsyncStagehand + self, remaining_retries: int, retry_after: str, timeout: float, async_client: AsyncBrowserbase ) -> None: headers = httpx.Headers({"retry-after": retry_after}) options = FinalRequestOptions(method="get", url="/foo", max_retries=3) @@ -1610,7 +1604,7 @@ async def test_parse_retry_after_header( @mock.patch("stagehand._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) @pytest.mark.respx(base_url=base_url) async def test_retrying_timeout_errors_doesnt_leak( - self, respx_mock: MockRouter, async_client: AsyncStagehand + self, respx_mock: MockRouter, async_client: AsyncBrowserbase ) -> None: respx_mock.post("/sessions/start").mock(side_effect=httpx.TimeoutException("Test timeout error")) @@ -1622,7 +1616,7 @@ async def test_retrying_timeout_errors_doesnt_leak( @mock.patch("stagehand._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) @pytest.mark.respx(base_url=base_url) async def test_retrying_status_errors_doesnt_leak( - self, respx_mock: MockRouter, async_client: AsyncStagehand + self, respx_mock: MockRouter, async_client: AsyncBrowserbase ) -> None: respx_mock.post("/sessions/start").mock(return_value=httpx.Response(500)) @@ -1636,7 +1630,7 @@ async def test_retrying_status_errors_doesnt_leak( @pytest.mark.parametrize("failure_mode", ["status", "exception"]) async def test_retries_taken( self, - async_client: AsyncStagehand, + async_client: AsyncBrowserbase, failures_before_success: int, failure_mode: Literal["status", "exception"], respx_mock: MockRouter, @@ -1665,7 +1659,7 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: @mock.patch("stagehand._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) @pytest.mark.respx(base_url=base_url) async def test_omit_retry_count_header( - self, async_client: AsyncStagehand, failures_before_success: int, respx_mock: MockRouter + self, async_client: AsyncBrowserbase, failures_before_success: int, respx_mock: MockRouter ) -> None: client = async_client.with_options(max_retries=4) @@ -1690,7 +1684,7 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: @mock.patch("stagehand._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) @pytest.mark.respx(base_url=base_url) async def test_overwrite_retry_count_header( - self, async_client: AsyncStagehand, failures_before_success: int, respx_mock: MockRouter + self, async_client: AsyncBrowserbase, failures_before_success: int, respx_mock: MockRouter ) -> None: client = async_client.with_options(max_retries=4) @@ -1738,7 +1732,7 @@ async def test_default_client_creation(self) -> None: ) @pytest.mark.respx(base_url=base_url) - async def test_follow_redirects(self, respx_mock: MockRouter, async_client: AsyncStagehand) -> None: + async def test_follow_redirects(self, respx_mock: MockRouter, async_client: AsyncBrowserbase) -> None: # Test that the default follow_redirects=True allows following redirects respx_mock.post("/redirect").mock( return_value=httpx.Response(302, headers={"Location": f"{base_url}/redirected"}) @@ -1750,7 +1744,7 @@ async def test_follow_redirects(self, respx_mock: MockRouter, async_client: Asyn assert response.json() == {"status": "ok"} @pytest.mark.respx(base_url=base_url) - async def test_follow_redirects_disabled(self, respx_mock: MockRouter, async_client: AsyncStagehand) -> None: + async def test_follow_redirects_disabled(self, respx_mock: MockRouter, async_client: AsyncBrowserbase) -> None: # Test that follow_redirects=False prevents following redirects respx_mock.post("/redirect").mock( return_value=httpx.Response(302, headers={"Location": f"{base_url}/redirected"}) diff --git a/tests/test_response.py b/tests/test_response.py index 8da886ce..5caa13ec 100644 --- a/tests/test_response.py +++ b/tests/test_response.py @@ -6,7 +6,7 @@ import pytest import pydantic -from stagehand import BaseModel, Stagehand, AsyncStagehand +from stagehand import BaseModel, Browserbase, AsyncBrowserbase from stagehand._response import ( APIResponse, BaseAPIResponse, @@ -56,7 +56,7 @@ def test_extract_response_type_binary_response() -> None: class PydanticModel(pydantic.BaseModel): ... -def test_response_parse_mismatched_basemodel(client: Stagehand) -> None: +def test_response_parse_mismatched_basemodel(client: Browserbase) -> None: response = APIResponse( raw=httpx.Response(200, content=b"foo"), client=client, @@ -74,7 +74,7 @@ def test_response_parse_mismatched_basemodel(client: Stagehand) -> None: @pytest.mark.asyncio -async def test_async_response_parse_mismatched_basemodel(async_client: AsyncStagehand) -> None: +async def test_async_response_parse_mismatched_basemodel(async_client: AsyncBrowserbase) -> None: response = AsyncAPIResponse( raw=httpx.Response(200, content=b"foo"), client=async_client, @@ -91,7 +91,7 @@ async def test_async_response_parse_mismatched_basemodel(async_client: AsyncStag await response.parse(to=PydanticModel) -def test_response_parse_custom_stream(client: Stagehand) -> None: +def test_response_parse_custom_stream(client: Browserbase) -> None: response = APIResponse( raw=httpx.Response(200, content=b"foo"), client=client, @@ -106,7 +106,7 @@ def test_response_parse_custom_stream(client: Stagehand) -> None: @pytest.mark.asyncio -async def test_async_response_parse_custom_stream(async_client: AsyncStagehand) -> None: +async def test_async_response_parse_custom_stream(async_client: AsyncBrowserbase) -> None: response = AsyncAPIResponse( raw=httpx.Response(200, content=b"foo"), client=async_client, @@ -125,7 +125,7 @@ class CustomModel(BaseModel): bar: int -def test_response_parse_custom_model(client: Stagehand) -> None: +def test_response_parse_custom_model(client: Browserbase) -> None: response = APIResponse( raw=httpx.Response(200, content=json.dumps({"foo": "hello!", "bar": 2})), client=client, @@ -141,7 +141,7 @@ def test_response_parse_custom_model(client: Stagehand) -> None: @pytest.mark.asyncio -async def test_async_response_parse_custom_model(async_client: AsyncStagehand) -> None: +async def test_async_response_parse_custom_model(async_client: AsyncBrowserbase) -> None: response = AsyncAPIResponse( raw=httpx.Response(200, content=json.dumps({"foo": "hello!", "bar": 2})), client=async_client, @@ -156,7 +156,7 @@ async def test_async_response_parse_custom_model(async_client: AsyncStagehand) - assert obj.bar == 2 -def test_response_parse_annotated_type(client: Stagehand) -> None: +def test_response_parse_annotated_type(client: Browserbase) -> None: response = APIResponse( raw=httpx.Response(200, content=json.dumps({"foo": "hello!", "bar": 2})), client=client, @@ -173,7 +173,7 @@ def test_response_parse_annotated_type(client: Stagehand) -> None: assert obj.bar == 2 -async def test_async_response_parse_annotated_type(async_client: AsyncStagehand) -> None: +async def test_async_response_parse_annotated_type(async_client: AsyncBrowserbase) -> None: response = AsyncAPIResponse( raw=httpx.Response(200, content=json.dumps({"foo": "hello!", "bar": 2})), client=async_client, @@ -201,7 +201,7 @@ async def test_async_response_parse_annotated_type(async_client: AsyncStagehand) ("FalSe", False), ], ) -def test_response_parse_bool(client: Stagehand, content: str, expected: bool) -> None: +def test_response_parse_bool(client: Browserbase, content: str, expected: bool) -> None: response = APIResponse( raw=httpx.Response(200, content=content), client=client, @@ -226,7 +226,7 @@ def test_response_parse_bool(client: Stagehand, content: str, expected: bool) -> ("FalSe", False), ], ) -async def test_async_response_parse_bool(client: AsyncStagehand, content: str, expected: bool) -> None: +async def test_async_response_parse_bool(client: AsyncBrowserbase, content: str, expected: bool) -> None: response = AsyncAPIResponse( raw=httpx.Response(200, content=content), client=client, @@ -245,7 +245,7 @@ class OtherModel(BaseModel): @pytest.mark.parametrize("client", [False], indirect=True) # loose validation -def test_response_parse_expect_model_union_non_json_content(client: Stagehand) -> None: +def test_response_parse_expect_model_union_non_json_content(client: Browserbase) -> None: response = APIResponse( raw=httpx.Response(200, content=b"foo", headers={"Content-Type": "application/text"}), client=client, @@ -262,7 +262,7 @@ def test_response_parse_expect_model_union_non_json_content(client: Stagehand) - @pytest.mark.asyncio @pytest.mark.parametrize("async_client", [False], indirect=True) # loose validation -async def test_async_response_parse_expect_model_union_non_json_content(async_client: AsyncStagehand) -> None: +async def test_async_response_parse_expect_model_union_non_json_content(async_client: AsyncBrowserbase) -> None: response = AsyncAPIResponse( raw=httpx.Response(200, content=b"foo", headers={"Content-Type": "application/text"}), client=async_client, diff --git a/tests/test_streaming.py b/tests/test_streaming.py index 01423cbb..5379ff9d 100644 --- a/tests/test_streaming.py +++ b/tests/test_streaming.py @@ -5,13 +5,13 @@ import httpx import pytest -from stagehand import Stagehand, AsyncStagehand +from stagehand import Browserbase, AsyncBrowserbase from stagehand._streaming import Stream, AsyncStream, ServerSentEvent @pytest.mark.asyncio @pytest.mark.parametrize("sync", [True, False], ids=["sync", "async"]) -async def test_basic(sync: bool, client: Stagehand, async_client: AsyncStagehand) -> None: +async def test_basic(sync: bool, client: Browserbase, async_client: AsyncBrowserbase) -> None: def body() -> Iterator[bytes]: yield b"event: completion\n" yield b'data: {"foo":true}\n' @@ -28,7 +28,7 @@ def body() -> Iterator[bytes]: @pytest.mark.asyncio @pytest.mark.parametrize("sync", [True, False], ids=["sync", "async"]) -async def test_data_missing_event(sync: bool, client: Stagehand, async_client: AsyncStagehand) -> None: +async def test_data_missing_event(sync: bool, client: Browserbase, async_client: AsyncBrowserbase) -> None: def body() -> Iterator[bytes]: yield b'data: {"foo":true}\n' yield b"\n" @@ -44,7 +44,7 @@ def body() -> Iterator[bytes]: @pytest.mark.asyncio @pytest.mark.parametrize("sync", [True, False], ids=["sync", "async"]) -async def test_event_missing_data(sync: bool, client: Stagehand, async_client: AsyncStagehand) -> None: +async def test_event_missing_data(sync: bool, client: Browserbase, async_client: AsyncBrowserbase) -> None: def body() -> Iterator[bytes]: yield b"event: ping\n" yield b"\n" @@ -60,7 +60,7 @@ def body() -> Iterator[bytes]: @pytest.mark.asyncio @pytest.mark.parametrize("sync", [True, False], ids=["sync", "async"]) -async def test_multiple_events(sync: bool, client: Stagehand, async_client: AsyncStagehand) -> None: +async def test_multiple_events(sync: bool, client: Browserbase, async_client: AsyncBrowserbase) -> None: def body() -> Iterator[bytes]: yield b"event: ping\n" yield b"\n" @@ -82,7 +82,7 @@ def body() -> Iterator[bytes]: @pytest.mark.asyncio @pytest.mark.parametrize("sync", [True, False], ids=["sync", "async"]) -async def test_multiple_events_with_data(sync: bool, client: Stagehand, async_client: AsyncStagehand) -> None: +async def test_multiple_events_with_data(sync: bool, client: Browserbase, async_client: AsyncBrowserbase) -> None: def body() -> Iterator[bytes]: yield b"event: ping\n" yield b'data: {"foo":true}\n' @@ -106,7 +106,9 @@ def body() -> Iterator[bytes]: @pytest.mark.asyncio @pytest.mark.parametrize("sync", [True, False], ids=["sync", "async"]) -async def test_multiple_data_lines_with_empty_line(sync: bool, client: Stagehand, async_client: AsyncStagehand) -> None: +async def test_multiple_data_lines_with_empty_line( + sync: bool, client: Browserbase, async_client: AsyncBrowserbase +) -> None: def body() -> Iterator[bytes]: yield b"event: ping\n" yield b"data: {\n" @@ -128,7 +130,9 @@ def body() -> Iterator[bytes]: @pytest.mark.asyncio @pytest.mark.parametrize("sync", [True, False], ids=["sync", "async"]) -async def test_data_json_escaped_double_new_line(sync: bool, client: Stagehand, async_client: AsyncStagehand) -> None: +async def test_data_json_escaped_double_new_line( + sync: bool, client: Browserbase, async_client: AsyncBrowserbase +) -> None: def body() -> Iterator[bytes]: yield b"event: ping\n" yield b'data: {"foo": "my long\\n\\ncontent"}' @@ -145,7 +149,7 @@ def body() -> Iterator[bytes]: @pytest.mark.asyncio @pytest.mark.parametrize("sync", [True, False], ids=["sync", "async"]) -async def test_multiple_data_lines(sync: bool, client: Stagehand, async_client: AsyncStagehand) -> None: +async def test_multiple_data_lines(sync: bool, client: Browserbase, async_client: AsyncBrowserbase) -> None: def body() -> Iterator[bytes]: yield b"event: ping\n" yield b"data: {\n" @@ -165,8 +169,8 @@ def body() -> Iterator[bytes]: @pytest.mark.parametrize("sync", [True, False], ids=["sync", "async"]) async def test_special_new_line_character( sync: bool, - client: Stagehand, - async_client: AsyncStagehand, + client: Browserbase, + async_client: AsyncBrowserbase, ) -> None: def body() -> Iterator[bytes]: yield b'data: {"content":" culpa"}\n' @@ -196,8 +200,8 @@ def body() -> Iterator[bytes]: @pytest.mark.parametrize("sync", [True, False], ids=["sync", "async"]) async def test_multi_byte_character_multiple_chunks( sync: bool, - client: Stagehand, - async_client: AsyncStagehand, + client: Browserbase, + async_client: AsyncBrowserbase, ) -> None: def body() -> Iterator[bytes]: yield b'data: {"content":"' @@ -237,8 +241,8 @@ def make_event_iterator( content: Iterator[bytes], *, sync: bool, - client: Stagehand, - async_client: AsyncStagehand, + client: Browserbase, + async_client: AsyncBrowserbase, ) -> Iterator[ServerSentEvent] | AsyncIterator[ServerSentEvent]: if sync: return Stream(cast_to=object, client=client, response=httpx.Response(200, content=content))._iter_events() From ebc268fe492793bdf43e1e5069d05b2815c7ca2d Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 16 Dec 2025 19:14:04 +0000 Subject: [PATCH 14/88] feat(api): manual updates --- .github/workflows/publish-pypi.yml | 31 +++++++++++++ .github/workflows/release-doctor.yml | 21 +++++++++ .release-please-manifest.json | 3 ++ .stats.yml | 2 +- CONTRIBUTING.md | 4 +- README.md | 14 +++--- bin/check-release-environment | 21 +++++++++ pyproject.toml | 6 +-- release-please-config.json | 66 ++++++++++++++++++++++++++++ src/stagehand/_version.py | 2 +- src/stagehand/resources/sessions.py | 8 ++-- 11 files changed, 160 insertions(+), 18 deletions(-) create mode 100644 .github/workflows/publish-pypi.yml create mode 100644 .github/workflows/release-doctor.yml create mode 100644 .release-please-manifest.json create mode 100644 bin/check-release-environment create mode 100644 release-please-config.json diff --git a/.github/workflows/publish-pypi.yml b/.github/workflows/publish-pypi.yml new file mode 100644 index 00000000..4041896e --- /dev/null +++ b/.github/workflows/publish-pypi.yml @@ -0,0 +1,31 @@ +# This workflow is triggered when a GitHub release is created. +# It can also be run manually to re-publish to PyPI in case it failed for some reason. +# You can run this workflow by navigating to https://www.github.com/browserbase/stagehand-python/actions/workflows/publish-pypi.yml +name: Publish PyPI +on: + workflow_dispatch: + + release: + types: [published] + +jobs: + publish: + name: publish + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Install Rye + run: | + curl -sSf https://rye.astral.sh/get | bash + echo "$HOME/.rye/shims" >> $GITHUB_PATH + env: + RYE_VERSION: '0.44.0' + RYE_INSTALL_OPTION: '--yes' + + - name: Publish to PyPI + run: | + bash ./bin/publish-pypi + env: + PYPI_TOKEN: ${{ secrets.BROWSERBASE_PYPI_TOKEN || secrets.PYPI_TOKEN }} diff --git a/.github/workflows/release-doctor.yml b/.github/workflows/release-doctor.yml new file mode 100644 index 00000000..0a5d3ef2 --- /dev/null +++ b/.github/workflows/release-doctor.yml @@ -0,0 +1,21 @@ +name: Release Doctor +on: + pull_request: + branches: + - stainless + workflow_dispatch: + +jobs: + release_doctor: + name: release doctor + runs-on: ubuntu-latest + if: github.repository == 'browserbase/stagehand-python' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch' || startsWith(github.head_ref, 'release-please') || github.head_ref == 'next') + + steps: + - uses: actions/checkout@v4 + + - name: Check release environment + run: | + bash ./bin/check-release-environment + env: + PYPI_TOKEN: ${{ secrets.BROWSERBASE_PYPI_TOKEN || secrets.PYPI_TOKEN }} diff --git a/.release-please-manifest.json b/.release-please-manifest.json new file mode 100644 index 00000000..1332969b --- /dev/null +++ b/.release-please-manifest.json @@ -0,0 +1,3 @@ +{ + ".": "0.0.1" +} \ No newline at end of file diff --git a/.stats.yml b/.stats.yml index 18b7af83..ae2d02d7 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 7 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fstagehand-0c12f985340be2a9287e8e01ff8733f7f2d02e019149d1ae95f1a8f8798c6690.yml openapi_spec_hash: efb79934e1dc63763dd4e8493b825273 -config_hash: 1548ab91b7e8621f7fa79e8cff0c3f93 +config_hash: 905fc70fd4344c8631aab6754bffd883 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 10d07c5d..89d0b51f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -62,7 +62,7 @@ If you’d like to use the repository from source, you can either install from g To install via git: ```sh -$ pip install git+ssh://git@github.com/stainless-sdks/stagehand-python.git +$ pip install git+ssh://git@github.com/browserbase/stagehand-python#stainless.git ``` Alternatively, you can build from source and install the wheel file: @@ -120,7 +120,7 @@ the changes aren't made through the automated pipeline, you may want to make rel ### Publish with a GitHub workflow -You can release to package managers by using [the `Publish PyPI` GitHub action](https://www.github.com/stainless-sdks/stagehand-python/actions/workflows/publish-pypi.yml). This requires a setup organization or repository secret to be set up. +You can release to package managers by using [the `Publish PyPI` GitHub action](https://www.github.com/browserbase/stagehand-python/actions/workflows/publish-pypi.yml). This requires a setup organization or repository secret to be set up. ### Publish manually diff --git a/README.md b/README.md index 44331a78..52b09a5e 100644 --- a/README.md +++ b/README.md @@ -16,8 +16,8 @@ The REST API documentation can be found on [browserbase.com](https://browserbase ## Installation ```sh -# install from this staging repo -pip install git+ssh://git@github.com/stainless-sdks/stagehand-python.git +# install from the production repo +pip install git+ssh://git@github.com/browserbase/stagehand-python#stainless.git ``` > [!NOTE] @@ -83,8 +83,8 @@ By default, the async client uses `httpx` for HTTP requests. However, for improv You can enable this by installing `aiohttp`: ```sh -# install from this staging repo -pip install 'stagehand[aiohttp] @ git+ssh://git@github.com/stainless-sdks/stagehand-python.git' +# install from the production repo +pip install 'stagehand[aiohttp] @ git+ssh://git@github.com/browserbase/stagehand-python#stainless.git' ``` Then you can enable it by instantiating the client with `http_client=DefaultAioHttpClient()`: @@ -273,9 +273,9 @@ session = response.parse() # get the object that `sessions.start()` would have print(session.available) ``` -These methods return an [`APIResponse`](https://github.com/stainless-sdks/stagehand-python/tree/main/src/stagehand/_response.py) object. +These methods return an [`APIResponse`](https://github.com/browserbase/stagehand-python/tree/stainless/src/stagehand/_response.py) object. -The async client returns an [`AsyncAPIResponse`](https://github.com/stainless-sdks/stagehand-python/tree/main/src/stagehand/_response.py) with the same structure, the only difference being `await`able methods for reading the response content. +The async client returns an [`AsyncAPIResponse`](https://github.com/browserbase/stagehand-python/tree/stainless/src/stagehand/_response.py) with the same structure, the only difference being `await`able methods for reading the response content. #### `.with_streaming_response` @@ -381,7 +381,7 @@ This package generally follows [SemVer](https://semver.org/spec/v2.0.0.html) con We take backwards-compatibility seriously and work hard to ensure you can rely on a smooth upgrade experience. -We are keen for your feedback; please open an [issue](https://www.github.com/stainless-sdks/stagehand-python/issues) with questions, bugs, or suggestions. +We are keen for your feedback; please open an [issue](https://www.github.com/browserbase/stagehand-python/issues) with questions, bugs, or suggestions. ### Determining the installed version diff --git a/bin/check-release-environment b/bin/check-release-environment new file mode 100644 index 00000000..b845b0f4 --- /dev/null +++ b/bin/check-release-environment @@ -0,0 +1,21 @@ +#!/usr/bin/env bash + +errors=() + +if [ -z "${PYPI_TOKEN}" ]; then + errors+=("The PYPI_TOKEN secret has not been set. Please set it in either this repository's secrets or your organization secrets.") +fi + +lenErrors=${#errors[@]} + +if [[ lenErrors -gt 0 ]]; then + echo -e "Found the following errors in the release environment:\n" + + for error in "${errors[@]}"; do + echo -e "- $error\n" + done + + exit 1 +fi + +echo "The environment is ready to push releases!" diff --git a/pyproject.toml b/pyproject.toml index c26da1f7..f3cc4f7b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,8 +37,8 @@ classifiers = [ ] [project.urls] -Homepage = "https://github.com/stainless-sdks/stagehand-python" -Repository = "https://github.com/stainless-sdks/stagehand-python" +Homepage = "https://github.com/browserbase/stagehand-python" +Repository = "https://github.com/browserbase/stagehand-python" [project.optional-dependencies] aiohttp = ["aiohttp", "httpx_aiohttp>=0.1.9"] @@ -126,7 +126,7 @@ path = "README.md" [[tool.hatch.metadata.hooks.fancy-pypi-readme.substitutions]] # replace relative links with absolute links pattern = '\[(.+?)\]\(((?!https?://)\S+?)\)' -replacement = '[\1](https://github.com/stainless-sdks/stagehand-python/tree/main/\g<2>)' +replacement = '[\1](https://github.com/browserbase/stagehand-python/tree/stainless/\g<2>)' [tool.pytest.ini_options] testpaths = ["tests"] diff --git a/release-please-config.json b/release-please-config.json new file mode 100644 index 00000000..ff5a43c4 --- /dev/null +++ b/release-please-config.json @@ -0,0 +1,66 @@ +{ + "packages": { + ".": {} + }, + "$schema": "https://raw.githubusercontent.com/stainless-api/release-please/main/schemas/config.json", + "include-v-in-tag": true, + "include-component-in-tag": false, + "versioning": "prerelease", + "prerelease": true, + "bump-minor-pre-major": true, + "bump-patch-for-minor-pre-major": false, + "pull-request-header": "Automated Release PR", + "pull-request-title-pattern": "release: ${version}", + "changelog-sections": [ + { + "type": "feat", + "section": "Features" + }, + { + "type": "fix", + "section": "Bug Fixes" + }, + { + "type": "perf", + "section": "Performance Improvements" + }, + { + "type": "revert", + "section": "Reverts" + }, + { + "type": "chore", + "section": "Chores" + }, + { + "type": "docs", + "section": "Documentation" + }, + { + "type": "style", + "section": "Styles" + }, + { + "type": "refactor", + "section": "Refactors" + }, + { + "type": "test", + "section": "Tests", + "hidden": true + }, + { + "type": "build", + "section": "Build System" + }, + { + "type": "ci", + "section": "Continuous Integration", + "hidden": true + } + ], + "release-type": "python", + "extra-files": [ + "src/stagehand/_version.py" + ] +} \ No newline at end of file diff --git a/src/stagehand/_version.py b/src/stagehand/_version.py index af4c4034..e1b7ed63 100644 --- a/src/stagehand/_version.py +++ b/src/stagehand/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "stagehand" -__version__ = "0.0.1" +__version__ = "0.0.1" # x-release-please-version diff --git a/src/stagehand/resources/sessions.py b/src/stagehand/resources/sessions.py index 0135a438..b229fd92 100644 --- a/src/stagehand/resources/sessions.py +++ b/src/stagehand/resources/sessions.py @@ -44,7 +44,7 @@ def with_raw_response(self) -> SessionsResourceWithRawResponse: This property can be used as a prefix for any HTTP method call to return the raw response object instead of the parsed content. - For more information, see https://www.github.com/stainless-sdks/stagehand-python#accessing-raw-response-data-eg-headers + For more information, see https://www.github.com/browserbase/stagehand-python#accessing-raw-response-data-eg-headers """ return SessionsResourceWithRawResponse(self) @@ -53,7 +53,7 @@ def with_streaming_response(self) -> SessionsResourceWithStreamingResponse: """ An alternative to `.with_raw_response` that doesn't eagerly read the response body. - For more information, see https://www.github.com/stainless-sdks/stagehand-python#with_streaming_response + For more information, see https://www.github.com/browserbase/stagehand-python#with_streaming_response """ return SessionsResourceWithStreamingResponse(self) @@ -453,7 +453,7 @@ def with_raw_response(self) -> AsyncSessionsResourceWithRawResponse: This property can be used as a prefix for any HTTP method call to return the raw response object instead of the parsed content. - For more information, see https://www.github.com/stainless-sdks/stagehand-python#accessing-raw-response-data-eg-headers + For more information, see https://www.github.com/browserbase/stagehand-python#accessing-raw-response-data-eg-headers """ return AsyncSessionsResourceWithRawResponse(self) @@ -462,7 +462,7 @@ def with_streaming_response(self) -> AsyncSessionsResourceWithStreamingResponse: """ An alternative to `.with_raw_response` that doesn't eagerly read the response body. - For more information, see https://www.github.com/stainless-sdks/stagehand-python#with_streaming_response + For more information, see https://www.github.com/browserbase/stagehand-python#with_streaming_response """ return AsyncSessionsResourceWithStreamingResponse(self) From 51e8162c99ff501181a95190499f684b34c15590 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 16 Dec 2025 19:18:29 +0000 Subject: [PATCH 15/88] feat(api): manual updates --- .github/workflows/publish-pypi.yml | 2 +- .github/workflows/release-doctor.yml | 2 +- .stats.yml | 2 +- LICENSE | 2 +- README.md | 56 +++--- SECURITY.md | 2 +- pyproject.toml | 4 +- src/stagehand/__init__.py | 12 +- src/stagehand/_client.py | 62 +++---- src/stagehand/_exceptions.py | 4 +- src/stagehand/_resource.py | 10 +- src/stagehand/_response.py | 4 +- src/stagehand/_streaming.py | 6 +- src/stagehand/_utils/_logs.py | 2 +- tests/api_resources/test_sessions.py | 134 +++++++-------- tests/conftest.py | 10 +- tests/test_client.py | 246 +++++++++++++-------------- tests/test_response.py | 26 +-- tests/test_streaming.py | 34 ++-- 19 files changed, 302 insertions(+), 318 deletions(-) diff --git a/.github/workflows/publish-pypi.yml b/.github/workflows/publish-pypi.yml index 4041896e..0b389f9c 100644 --- a/.github/workflows/publish-pypi.yml +++ b/.github/workflows/publish-pypi.yml @@ -28,4 +28,4 @@ jobs: run: | bash ./bin/publish-pypi env: - PYPI_TOKEN: ${{ secrets.BROWSERBASE_PYPI_TOKEN || secrets.PYPI_TOKEN }} + PYPI_TOKEN: ${{ secrets.STAGEHAND_PYPI_TOKEN || secrets.PYPI_TOKEN }} diff --git a/.github/workflows/release-doctor.yml b/.github/workflows/release-doctor.yml index 0a5d3ef2..2caaf57f 100644 --- a/.github/workflows/release-doctor.yml +++ b/.github/workflows/release-doctor.yml @@ -18,4 +18,4 @@ jobs: run: | bash ./bin/check-release-environment env: - PYPI_TOKEN: ${{ secrets.BROWSERBASE_PYPI_TOKEN || secrets.PYPI_TOKEN }} + PYPI_TOKEN: ${{ secrets.STAGEHAND_PYPI_TOKEN || secrets.PYPI_TOKEN }} diff --git a/.stats.yml b/.stats.yml index ae2d02d7..8741890e 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 7 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fstagehand-0c12f985340be2a9287e8e01ff8733f7f2d02e019149d1ae95f1a8f8798c6690.yml openapi_spec_hash: efb79934e1dc63763dd4e8493b825273 -config_hash: 905fc70fd4344c8631aab6754bffd883 +config_hash: 1de7cb9bd4dc46fe3e20b637bc534908 diff --git a/LICENSE b/LICENSE index 2cec9d4b..6b24314a 100644 --- a/LICENSE +++ b/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright 2025 Browserbase + Copyright 2025 Stagehand Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/README.md b/README.md index 52b09a5e..3a041fa0 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,9 @@ -# Browserbase Python API library +# Stagehand Python API library [![PyPI version](https://img.shields.io/pypi/v/stagehand.svg?label=pypi%20(stable))](https://pypi.org/project/stagehand/) -The Browserbase Python library provides convenient access to the Browserbase REST API from any Python 3.9+ +The Stagehand Python library provides convenient access to the Stagehand REST API from any Python 3.9+ application. The library includes type definitions for all request params and response fields, and offers both synchronous and asynchronous clients powered by [httpx](https://github.com/encode/httpx). @@ -11,7 +11,7 @@ It is generated with [Stainless](https://www.stainless.com/). ## Documentation -The REST API documentation can be found on [browserbase.com](https://browserbase.com). The full API of this library can be found in [api.md](api.md). +The REST API documentation can be found on [docs.stagehand.dev](https://docs.stagehand.dev). The full API of this library can be found in [api.md](api.md). ## Installation @@ -29,9 +29,9 @@ The full API of this library can be found in [api.md](api.md). ```python import os -from stagehand import Browserbase +from stagehand import Stagehand -client = Browserbase( +client = Stagehand( api_key=os.environ.get("STAGEHAND_API_KEY"), # This is the default and can be omitted # or 'production' | 'local'; defaults to "production". environment="dev", @@ -50,14 +50,14 @@ so that your API Key is not stored in source control. ## Async usage -Simply import `AsyncBrowserbase` instead of `Browserbase` and use `await` with each API call: +Simply import `AsyncStagehand` instead of `Stagehand` and use `await` with each API call: ```python import os import asyncio -from stagehand import AsyncBrowserbase +from stagehand import AsyncStagehand -client = AsyncBrowserbase( +client = AsyncStagehand( api_key=os.environ.get("STAGEHAND_API_KEY"), # This is the default and can be omitted # or 'production' | 'local'; defaults to "production". environment="dev", @@ -93,11 +93,11 @@ Then you can enable it by instantiating the client with `http_client=DefaultAioH import os import asyncio from stagehand import DefaultAioHttpClient -from stagehand import AsyncBrowserbase +from stagehand import AsyncStagehand async def main() -> None: - async with AsyncBrowserbase( + async with AsyncStagehand( api_key=os.environ.get("STAGEHAND_API_KEY"), # This is the default and can be omitted http_client=DefaultAioHttpClient(), ) as client: @@ -124,9 +124,9 @@ Typed requests and responses provide autocomplete and documentation within your Nested parameters are dictionaries, typed using `TypedDict`, for example: ```python -from stagehand import Browserbase +from stagehand import Stagehand -client = Browserbase() +client = Stagehand() response = client.sessions.start( env="LOCAL", @@ -146,9 +146,9 @@ All errors inherit from `stagehand.APIError`. ```python import stagehand -from stagehand import Browserbase +from stagehand import Stagehand -client = Browserbase() +client = Stagehand() try: client.sessions.start( @@ -187,10 +187,10 @@ Connection errors (for example, due to a network connectivity problem), 408 Requ You can use the `max_retries` option to configure or disable retry settings: ```python -from stagehand import Browserbase +from stagehand import Stagehand # Configure the default for all requests: -client = Browserbase( +client = Stagehand( # default is 2 max_retries=0, ) @@ -207,16 +207,16 @@ By default requests time out after 1 minute. You can configure this with a `time which accepts a float or an [`httpx.Timeout`](https://www.python-httpx.org/advanced/timeouts/#fine-tuning-the-configuration) object: ```python -from stagehand import Browserbase +from stagehand import Stagehand # Configure the default for all requests: -client = Browserbase( +client = Stagehand( # 20 seconds (default is 1 minute) timeout=20.0, ) # More granular control: -client = Browserbase( +client = Stagehand( timeout=httpx.Timeout(60.0, read=5.0, write=10.0, connect=2.0), ) @@ -236,10 +236,10 @@ Note that requests that time out are [retried twice by default](#retries). We use the standard library [`logging`](https://docs.python.org/3/library/logging.html) module. -You can enable logging by setting the environment variable `BROWSERBASE_LOG` to `info`. +You can enable logging by setting the environment variable `STAGEHAND_LOG` to `info`. ```shell -$ export BROWSERBASE_LOG=info +$ export STAGEHAND_LOG=info ``` Or to `debug` for more verbose logging. @@ -261,9 +261,9 @@ if response.my_field is None: The "raw" Response object can be accessed by prefixing `.with_raw_response.` to any HTTP method call, e.g., ```py -from stagehand import Browserbase +from stagehand import Stagehand -client = Browserbase() +client = Stagehand() response = client.sessions.with_raw_response.start( env="LOCAL", ) @@ -339,10 +339,10 @@ You can directly override the [httpx client](https://www.python-httpx.org/api/#c ```python import httpx -from stagehand import Browserbase, DefaultHttpxClient +from stagehand import Stagehand, DefaultHttpxClient -client = Browserbase( - # Or use the `BROWSERBASE_BASE_URL` env var +client = Stagehand( + # Or use the `STAGEHAND_BASE_URL` env var base_url="http://my.test.server.example.com:8083", http_client=DefaultHttpxClient( proxy="http://my.test.proxy.example.com", @@ -362,9 +362,9 @@ client.with_options(http_client=DefaultHttpxClient(...)) By default the library closes underlying HTTP connections whenever the client is [garbage collected](https://docs.python.org/3/reference/datamodel.html#object.__del__). You can manually close the client using the `.close()` method if desired, or with a context manager that closes when exiting. ```py -from stagehand import Browserbase +from stagehand import Stagehand -with Browserbase() as client: +with Stagehand() as client: # make requests here ... diff --git a/SECURITY.md b/SECURITY.md index be1c2db7..dcfc419a 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -16,7 +16,7 @@ before making any information public. ## Reporting Non-SDK Related Security Issues If you encounter security issues that are not directly related to SDKs but pertain to the services -or products provided by Browserbase, please follow the respective company's security reporting guidelines. +or products provided by Stagehand, please follow the respective company's security reporting guidelines. --- diff --git a/pyproject.toml b/pyproject.toml index f3cc4f7b..eb572236 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,11 +1,11 @@ [project] name = "stagehand" version = "0.0.1" -description = "The official Python library for the browserbase API" +description = "The official Python library for the stagehand API" dynamic = ["readme"] license = "Apache-2.0" authors = [ -{ name = "Browserbase", email = "" }, +{ name = "Stagehand", email = "" }, ] dependencies = [ diff --git a/src/stagehand/__init__.py b/src/stagehand/__init__.py index ed545a55..44c6dfcc 100644 --- a/src/stagehand/__init__.py +++ b/src/stagehand/__init__.py @@ -10,12 +10,12 @@ Client, Stream, Timeout, + Stagehand, Transport, AsyncClient, AsyncStream, - Browserbase, + AsyncStagehand, RequestOptions, - AsyncBrowserbase, ) from ._models import BaseModel from ._version import __title__, __version__ @@ -27,9 +27,9 @@ NotFoundError, APIStatusError, RateLimitError, + StagehandError, APITimeoutError, BadRequestError, - BrowserbaseError, APIConnectionError, AuthenticationError, InternalServerError, @@ -52,7 +52,7 @@ "not_given", "Omit", "omit", - "BrowserbaseError", + "StagehandError", "APIError", "APIStatusError", "APITimeoutError", @@ -72,8 +72,8 @@ "AsyncClient", "Stream", "AsyncStream", - "Browserbase", - "AsyncBrowserbase", + "Stagehand", + "AsyncStagehand", "ENVIRONMENTS", "file_from_path", "BaseModel", diff --git a/src/stagehand/_client.py b/src/stagehand/_client.py index d5773107..cf9608c6 100644 --- a/src/stagehand/_client.py +++ b/src/stagehand/_client.py @@ -23,7 +23,7 @@ from ._version import __version__ from .resources import sessions from ._streaming import Stream as Stream, AsyncStream as AsyncStream -from ._exceptions import APIStatusError, BrowserbaseError +from ._exceptions import APIStatusError, StagehandError from ._base_client import ( DEFAULT_MAX_RETRIES, SyncAPIClient, @@ -36,8 +36,8 @@ "Transport", "ProxiesTypes", "RequestOptions", - "Browserbase", - "AsyncBrowserbase", + "Stagehand", + "AsyncStagehand", "Client", "AsyncClient", ] @@ -49,10 +49,10 @@ } -class Browserbase(SyncAPIClient): +class Stagehand(SyncAPIClient): sessions: sessions.SessionsResource - with_raw_response: BrowserbaseWithRawResponse - with_streaming_response: BrowserbaseWithStreamedResponse + with_raw_response: StagehandWithRawResponse + with_streaming_response: StagehandWithStreamedResponse # client options api_key: str @@ -83,28 +83,28 @@ def __init__( # part of our public interface in the future. _strict_response_validation: bool = False, ) -> None: - """Construct a new synchronous Browserbase client instance. + """Construct a new synchronous Stagehand client instance. This automatically infers the `api_key` argument from the `STAGEHAND_API_KEY` environment variable if it is not provided. """ if api_key is None: api_key = os.environ.get("STAGEHAND_API_KEY") if api_key is None: - raise BrowserbaseError( + raise StagehandError( "The api_key client option must be set either by passing api_key to the client or by setting the STAGEHAND_API_KEY environment variable" ) self.api_key = api_key self._environment = environment - base_url_env = os.environ.get("BROWSERBASE_BASE_URL") + base_url_env = os.environ.get("STAGEHAND_BASE_URL") if is_given(base_url) and base_url is not None: # cast required because mypy doesn't understand the type narrowing base_url = cast("str | httpx.URL", base_url) # pyright: ignore[reportUnnecessaryCast] elif is_given(environment): if base_url_env and base_url is not None: raise ValueError( - "Ambiguous URL; The `BROWSERBASE_BASE_URL` env var and the `environment` argument are given. If you want to use the environment, you must pass base_url=None", + "Ambiguous URL; The `STAGEHAND_BASE_URL` env var and the `environment` argument are given. If you want to use the environment, you must pass base_url=None", ) try: @@ -133,8 +133,8 @@ def __init__( ) self.sessions = sessions.SessionsResource(self) - self.with_raw_response = BrowserbaseWithRawResponse(self) - self.with_streaming_response = BrowserbaseWithStreamedResponse(self) + self.with_raw_response = StagehandWithRawResponse(self) + self.with_streaming_response = StagehandWithStreamedResponse(self) @property @override @@ -243,10 +243,10 @@ def _make_status_error( return APIStatusError(err_msg, response=response, body=body) -class AsyncBrowserbase(AsyncAPIClient): +class AsyncStagehand(AsyncAPIClient): sessions: sessions.AsyncSessionsResource - with_raw_response: AsyncBrowserbaseWithRawResponse - with_streaming_response: AsyncBrowserbaseWithStreamedResponse + with_raw_response: AsyncStagehandWithRawResponse + with_streaming_response: AsyncStagehandWithStreamedResponse # client options api_key: str @@ -277,28 +277,28 @@ def __init__( # part of our public interface in the future. _strict_response_validation: bool = False, ) -> None: - """Construct a new async AsyncBrowserbase client instance. + """Construct a new async AsyncStagehand client instance. This automatically infers the `api_key` argument from the `STAGEHAND_API_KEY` environment variable if it is not provided. """ if api_key is None: api_key = os.environ.get("STAGEHAND_API_KEY") if api_key is None: - raise BrowserbaseError( + raise StagehandError( "The api_key client option must be set either by passing api_key to the client or by setting the STAGEHAND_API_KEY environment variable" ) self.api_key = api_key self._environment = environment - base_url_env = os.environ.get("BROWSERBASE_BASE_URL") + base_url_env = os.environ.get("STAGEHAND_BASE_URL") if is_given(base_url) and base_url is not None: # cast required because mypy doesn't understand the type narrowing base_url = cast("str | httpx.URL", base_url) # pyright: ignore[reportUnnecessaryCast] elif is_given(environment): if base_url_env and base_url is not None: raise ValueError( - "Ambiguous URL; The `BROWSERBASE_BASE_URL` env var and the `environment` argument are given. If you want to use the environment, you must pass base_url=None", + "Ambiguous URL; The `STAGEHAND_BASE_URL` env var and the `environment` argument are given. If you want to use the environment, you must pass base_url=None", ) try: @@ -327,8 +327,8 @@ def __init__( ) self.sessions = sessions.AsyncSessionsResource(self) - self.with_raw_response = AsyncBrowserbaseWithRawResponse(self) - self.with_streaming_response = AsyncBrowserbaseWithStreamedResponse(self) + self.with_raw_response = AsyncStagehandWithRawResponse(self) + self.with_streaming_response = AsyncStagehandWithStreamedResponse(self) @property @override @@ -437,26 +437,26 @@ def _make_status_error( return APIStatusError(err_msg, response=response, body=body) -class BrowserbaseWithRawResponse: - def __init__(self, client: Browserbase) -> None: +class StagehandWithRawResponse: + def __init__(self, client: Stagehand) -> None: self.sessions = sessions.SessionsResourceWithRawResponse(client.sessions) -class AsyncBrowserbaseWithRawResponse: - def __init__(self, client: AsyncBrowserbase) -> None: +class AsyncStagehandWithRawResponse: + def __init__(self, client: AsyncStagehand) -> None: self.sessions = sessions.AsyncSessionsResourceWithRawResponse(client.sessions) -class BrowserbaseWithStreamedResponse: - def __init__(self, client: Browserbase) -> None: +class StagehandWithStreamedResponse: + def __init__(self, client: Stagehand) -> None: self.sessions = sessions.SessionsResourceWithStreamingResponse(client.sessions) -class AsyncBrowserbaseWithStreamedResponse: - def __init__(self, client: AsyncBrowserbase) -> None: +class AsyncStagehandWithStreamedResponse: + def __init__(self, client: AsyncStagehand) -> None: self.sessions = sessions.AsyncSessionsResourceWithStreamingResponse(client.sessions) -Client = Browserbase +Client = Stagehand -AsyncClient = AsyncBrowserbase +AsyncClient = AsyncStagehand diff --git a/src/stagehand/_exceptions.py b/src/stagehand/_exceptions.py index 79b18ef7..52ee0590 100644 --- a/src/stagehand/_exceptions.py +++ b/src/stagehand/_exceptions.py @@ -18,11 +18,11 @@ ] -class BrowserbaseError(Exception): +class StagehandError(Exception): pass -class APIError(BrowserbaseError): +class APIError(StagehandError): message: str request: httpx.Request diff --git a/src/stagehand/_resource.py b/src/stagehand/_resource.py index fc2d2a11..0bdbdf73 100644 --- a/src/stagehand/_resource.py +++ b/src/stagehand/_resource.py @@ -8,13 +8,13 @@ import anyio if TYPE_CHECKING: - from ._client import Browserbase, AsyncBrowserbase + from ._client import Stagehand, AsyncStagehand class SyncAPIResource: - _client: Browserbase + _client: Stagehand - def __init__(self, client: Browserbase) -> None: + def __init__(self, client: Stagehand) -> None: self._client = client self._get = client.get self._post = client.post @@ -28,9 +28,9 @@ def _sleep(self, seconds: float) -> None: class AsyncAPIResource: - _client: AsyncBrowserbase + _client: AsyncStagehand - def __init__(self, client: AsyncBrowserbase) -> None: + def __init__(self, client: AsyncStagehand) -> None: self._client = client self._get = client.get self._post = client.post diff --git a/src/stagehand/_response.py b/src/stagehand/_response.py index 4c4f12e9..d1003417 100644 --- a/src/stagehand/_response.py +++ b/src/stagehand/_response.py @@ -29,7 +29,7 @@ from ._models import BaseModel, is_basemodel from ._constants import RAW_RESPONSE_HEADER, OVERRIDE_CAST_TO_HEADER from ._streaming import Stream, AsyncStream, is_stream_class_type, extract_stream_chunk_type -from ._exceptions import BrowserbaseError, APIResponseValidationError +from ._exceptions import StagehandError, APIResponseValidationError if TYPE_CHECKING: from ._models import FinalRequestOptions @@ -560,7 +560,7 @@ def __init__(self) -> None: ) -class StreamAlreadyConsumed(BrowserbaseError): +class StreamAlreadyConsumed(StagehandError): """ Attempted to read or stream content, but the content has already been streamed. diff --git a/src/stagehand/_streaming.py b/src/stagehand/_streaming.py index d107619d..69a9442b 100644 --- a/src/stagehand/_streaming.py +++ b/src/stagehand/_streaming.py @@ -12,7 +12,7 @@ from ._utils import extract_type_var_from_base if TYPE_CHECKING: - from ._client import Browserbase, AsyncBrowserbase + from ._client import Stagehand, AsyncStagehand _T = TypeVar("_T") @@ -30,7 +30,7 @@ def __init__( *, cast_to: type[_T], response: httpx.Response, - client: Browserbase, + client: Stagehand, ) -> None: self.response = response self._cast_to = cast_to @@ -93,7 +93,7 @@ def __init__( *, cast_to: type[_T], response: httpx.Response, - client: AsyncBrowserbase, + client: AsyncStagehand, ) -> None: self.response = response self._cast_to = cast_to diff --git a/src/stagehand/_utils/_logs.py b/src/stagehand/_utils/_logs.py index a8324d9d..2d011968 100644 --- a/src/stagehand/_utils/_logs.py +++ b/src/stagehand/_utils/_logs.py @@ -14,7 +14,7 @@ def _basic_config() -> None: def setup_logging() -> None: - env = os.environ.get("BROWSERBASE_LOG") + env = os.environ.get("STAGEHAND_LOG") if env == "debug": _basic_config() logger.setLevel(logging.DEBUG) diff --git a/tests/api_resources/test_sessions.py b/tests/api_resources/test_sessions.py index c538f244..2c0829d5 100644 --- a/tests/api_resources/test_sessions.py +++ b/tests/api_resources/test_sessions.py @@ -7,7 +7,7 @@ import pytest -from stagehand import Browserbase, AsyncBrowserbase +from stagehand import Stagehand, AsyncStagehand from tests.utils import assert_matches_type from stagehand.types import ( SessionActResponse, @@ -27,7 +27,7 @@ class TestSessions: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - def test_method_act(self, client: Browserbase) -> None: + def test_method_act(self, client: Stagehand) -> None: session = client.sessions.act( session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", input="click the sign in button", @@ -36,7 +36,7 @@ def test_method_act(self, client: Browserbase) -> None: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - def test_method_act_with_all_params(self, client: Browserbase) -> None: + def test_method_act_with_all_params(self, client: Stagehand) -> None: session = client.sessions.act( session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", input="click the sign in button", @@ -57,7 +57,7 @@ def test_method_act_with_all_params(self, client: Browserbase) -> None: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - def test_raw_response_act(self, client: Browserbase) -> None: + def test_raw_response_act(self, client: Stagehand) -> None: response = client.sessions.with_raw_response.act( session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", input="click the sign in button", @@ -70,7 +70,7 @@ def test_raw_response_act(self, client: Browserbase) -> None: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - def test_streaming_response_act(self, client: Browserbase) -> None: + def test_streaming_response_act(self, client: Stagehand) -> None: with client.sessions.with_streaming_response.act( session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", input="click the sign in button", @@ -85,7 +85,7 @@ def test_streaming_response_act(self, client: Browserbase) -> None: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - def test_path_params_act(self, client: Browserbase) -> None: + def test_path_params_act(self, client: Stagehand) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `session_id` but received ''"): client.sessions.with_raw_response.act( session_id="", @@ -94,7 +94,7 @@ def test_path_params_act(self, client: Browserbase) -> None: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - def test_method_end(self, client: Browserbase) -> None: + def test_method_end(self, client: Stagehand) -> None: session = client.sessions.end( "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", ) @@ -102,7 +102,7 @@ def test_method_end(self, client: Browserbase) -> None: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - def test_raw_response_end(self, client: Browserbase) -> None: + def test_raw_response_end(self, client: Stagehand) -> None: response = client.sessions.with_raw_response.end( "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", ) @@ -114,7 +114,7 @@ def test_raw_response_end(self, client: Browserbase) -> None: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - def test_streaming_response_end(self, client: Browserbase) -> None: + def test_streaming_response_end(self, client: Stagehand) -> None: with client.sessions.with_streaming_response.end( "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", ) as response: @@ -128,7 +128,7 @@ def test_streaming_response_end(self, client: Browserbase) -> None: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - def test_path_params_end(self, client: Browserbase) -> None: + def test_path_params_end(self, client: Stagehand) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `session_id` but received ''"): client.sessions.with_raw_response.end( "", @@ -136,7 +136,7 @@ def test_path_params_end(self, client: Browserbase) -> None: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - def test_method_execute_agent(self, client: Browserbase) -> None: + def test_method_execute_agent(self, client: Stagehand) -> None: session = client.sessions.execute_agent( session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", agent_config={}, @@ -146,7 +146,7 @@ def test_method_execute_agent(self, client: Browserbase) -> None: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - def test_method_execute_agent_with_all_params(self, client: Browserbase) -> None: + def test_method_execute_agent_with_all_params(self, client: Stagehand) -> None: session = client.sessions.execute_agent( session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", agent_config={ @@ -167,7 +167,7 @@ def test_method_execute_agent_with_all_params(self, client: Browserbase) -> None @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - def test_raw_response_execute_agent(self, client: Browserbase) -> None: + def test_raw_response_execute_agent(self, client: Stagehand) -> None: response = client.sessions.with_raw_response.execute_agent( session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", agent_config={}, @@ -181,7 +181,7 @@ def test_raw_response_execute_agent(self, client: Browserbase) -> None: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - def test_streaming_response_execute_agent(self, client: Browserbase) -> None: + def test_streaming_response_execute_agent(self, client: Stagehand) -> None: with client.sessions.with_streaming_response.execute_agent( session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", agent_config={}, @@ -197,7 +197,7 @@ def test_streaming_response_execute_agent(self, client: Browserbase) -> None: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - def test_path_params_execute_agent(self, client: Browserbase) -> None: + def test_path_params_execute_agent(self, client: Stagehand) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `session_id` but received ''"): client.sessions.with_raw_response.execute_agent( session_id="", @@ -207,7 +207,7 @@ def test_path_params_execute_agent(self, client: Browserbase) -> None: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - def test_method_extract(self, client: Browserbase) -> None: + def test_method_extract(self, client: Stagehand) -> None: session = client.sessions.extract( session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", ) @@ -215,7 +215,7 @@ def test_method_extract(self, client: Browserbase) -> None: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - def test_method_extract_with_all_params(self, client: Browserbase) -> None: + def test_method_extract_with_all_params(self, client: Stagehand) -> None: session = client.sessions.extract( session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", frame_id="frameId", @@ -237,7 +237,7 @@ def test_method_extract_with_all_params(self, client: Browserbase) -> None: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - def test_raw_response_extract(self, client: Browserbase) -> None: + def test_raw_response_extract(self, client: Stagehand) -> None: response = client.sessions.with_raw_response.extract( session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", ) @@ -249,7 +249,7 @@ def test_raw_response_extract(self, client: Browserbase) -> None: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - def test_streaming_response_extract(self, client: Browserbase) -> None: + def test_streaming_response_extract(self, client: Stagehand) -> None: with client.sessions.with_streaming_response.extract( session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", ) as response: @@ -263,7 +263,7 @@ def test_streaming_response_extract(self, client: Browserbase) -> None: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - def test_path_params_extract(self, client: Browserbase) -> None: + def test_path_params_extract(self, client: Stagehand) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `session_id` but received ''"): client.sessions.with_raw_response.extract( session_id="", @@ -271,7 +271,7 @@ def test_path_params_extract(self, client: Browserbase) -> None: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - def test_method_navigate(self, client: Browserbase) -> None: + def test_method_navigate(self, client: Stagehand) -> None: session = client.sessions.navigate( session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", url="https://example.com", @@ -280,7 +280,7 @@ def test_method_navigate(self, client: Browserbase) -> None: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - def test_method_navigate_with_all_params(self, client: Browserbase) -> None: + def test_method_navigate_with_all_params(self, client: Stagehand) -> None: session = client.sessions.navigate( session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", url="https://example.com", @@ -292,7 +292,7 @@ def test_method_navigate_with_all_params(self, client: Browserbase) -> None: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - def test_raw_response_navigate(self, client: Browserbase) -> None: + def test_raw_response_navigate(self, client: Stagehand) -> None: response = client.sessions.with_raw_response.navigate( session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", url="https://example.com", @@ -305,7 +305,7 @@ def test_raw_response_navigate(self, client: Browserbase) -> None: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - def test_streaming_response_navigate(self, client: Browserbase) -> None: + def test_streaming_response_navigate(self, client: Stagehand) -> None: with client.sessions.with_streaming_response.navigate( session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", url="https://example.com", @@ -320,7 +320,7 @@ def test_streaming_response_navigate(self, client: Browserbase) -> None: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - def test_path_params_navigate(self, client: Browserbase) -> None: + def test_path_params_navigate(self, client: Stagehand) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `session_id` but received ''"): client.sessions.with_raw_response.navigate( session_id="", @@ -329,7 +329,7 @@ def test_path_params_navigate(self, client: Browserbase) -> None: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - def test_method_observe(self, client: Browserbase) -> None: + def test_method_observe(self, client: Stagehand) -> None: session = client.sessions.observe( session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", ) @@ -337,7 +337,7 @@ def test_method_observe(self, client: Browserbase) -> None: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - def test_method_observe_with_all_params(self, client: Browserbase) -> None: + def test_method_observe_with_all_params(self, client: Stagehand) -> None: session = client.sessions.observe( session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", frame_id="frameId", @@ -358,7 +358,7 @@ def test_method_observe_with_all_params(self, client: Browserbase) -> None: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - def test_raw_response_observe(self, client: Browserbase) -> None: + def test_raw_response_observe(self, client: Stagehand) -> None: response = client.sessions.with_raw_response.observe( session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", ) @@ -370,7 +370,7 @@ def test_raw_response_observe(self, client: Browserbase) -> None: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - def test_streaming_response_observe(self, client: Browserbase) -> None: + def test_streaming_response_observe(self, client: Stagehand) -> None: with client.sessions.with_streaming_response.observe( session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", ) as response: @@ -384,7 +384,7 @@ def test_streaming_response_observe(self, client: Browserbase) -> None: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - def test_path_params_observe(self, client: Browserbase) -> None: + def test_path_params_observe(self, client: Stagehand) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `session_id` but received ''"): client.sessions.with_raw_response.observe( session_id="", @@ -392,7 +392,7 @@ def test_path_params_observe(self, client: Browserbase) -> None: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - def test_method_start(self, client: Browserbase) -> None: + def test_method_start(self, client: Stagehand) -> None: session = client.sessions.start( env="LOCAL", ) @@ -400,7 +400,7 @@ def test_method_start(self, client: Browserbase) -> None: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - def test_method_start_with_all_params(self, client: Browserbase) -> None: + def test_method_start_with_all_params(self, client: Stagehand) -> None: session = client.sessions.start( env="LOCAL", api_key="apiKey", @@ -416,7 +416,7 @@ def test_method_start_with_all_params(self, client: Browserbase) -> None: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - def test_raw_response_start(self, client: Browserbase) -> None: + def test_raw_response_start(self, client: Stagehand) -> None: response = client.sessions.with_raw_response.start( env="LOCAL", ) @@ -428,7 +428,7 @@ def test_raw_response_start(self, client: Browserbase) -> None: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - def test_streaming_response_start(self, client: Browserbase) -> None: + def test_streaming_response_start(self, client: Stagehand) -> None: with client.sessions.with_streaming_response.start( env="LOCAL", ) as response: @@ -448,7 +448,7 @@ class TestAsyncSessions: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - async def test_method_act(self, async_client: AsyncBrowserbase) -> None: + async def test_method_act(self, async_client: AsyncStagehand) -> None: session = await async_client.sessions.act( session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", input="click the sign in button", @@ -457,7 +457,7 @@ async def test_method_act(self, async_client: AsyncBrowserbase) -> None: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - async def test_method_act_with_all_params(self, async_client: AsyncBrowserbase) -> None: + async def test_method_act_with_all_params(self, async_client: AsyncStagehand) -> None: session = await async_client.sessions.act( session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", input="click the sign in button", @@ -478,7 +478,7 @@ async def test_method_act_with_all_params(self, async_client: AsyncBrowserbase) @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - async def test_raw_response_act(self, async_client: AsyncBrowserbase) -> None: + async def test_raw_response_act(self, async_client: AsyncStagehand) -> None: response = await async_client.sessions.with_raw_response.act( session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", input="click the sign in button", @@ -491,7 +491,7 @@ async def test_raw_response_act(self, async_client: AsyncBrowserbase) -> None: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - async def test_streaming_response_act(self, async_client: AsyncBrowserbase) -> None: + async def test_streaming_response_act(self, async_client: AsyncStagehand) -> None: async with async_client.sessions.with_streaming_response.act( session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", input="click the sign in button", @@ -506,7 +506,7 @@ async def test_streaming_response_act(self, async_client: AsyncBrowserbase) -> N @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - async def test_path_params_act(self, async_client: AsyncBrowserbase) -> None: + async def test_path_params_act(self, async_client: AsyncStagehand) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `session_id` but received ''"): await async_client.sessions.with_raw_response.act( session_id="", @@ -515,7 +515,7 @@ async def test_path_params_act(self, async_client: AsyncBrowserbase) -> None: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - async def test_method_end(self, async_client: AsyncBrowserbase) -> None: + async def test_method_end(self, async_client: AsyncStagehand) -> None: session = await async_client.sessions.end( "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", ) @@ -523,7 +523,7 @@ async def test_method_end(self, async_client: AsyncBrowserbase) -> None: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - async def test_raw_response_end(self, async_client: AsyncBrowserbase) -> None: + async def test_raw_response_end(self, async_client: AsyncStagehand) -> None: response = await async_client.sessions.with_raw_response.end( "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", ) @@ -535,7 +535,7 @@ async def test_raw_response_end(self, async_client: AsyncBrowserbase) -> None: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - async def test_streaming_response_end(self, async_client: AsyncBrowserbase) -> None: + async def test_streaming_response_end(self, async_client: AsyncStagehand) -> None: async with async_client.sessions.with_streaming_response.end( "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", ) as response: @@ -549,7 +549,7 @@ async def test_streaming_response_end(self, async_client: AsyncBrowserbase) -> N @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - async def test_path_params_end(self, async_client: AsyncBrowserbase) -> None: + async def test_path_params_end(self, async_client: AsyncStagehand) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `session_id` but received ''"): await async_client.sessions.with_raw_response.end( "", @@ -557,7 +557,7 @@ async def test_path_params_end(self, async_client: AsyncBrowserbase) -> None: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - async def test_method_execute_agent(self, async_client: AsyncBrowserbase) -> None: + async def test_method_execute_agent(self, async_client: AsyncStagehand) -> None: session = await async_client.sessions.execute_agent( session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", agent_config={}, @@ -567,7 +567,7 @@ async def test_method_execute_agent(self, async_client: AsyncBrowserbase) -> Non @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - async def test_method_execute_agent_with_all_params(self, async_client: AsyncBrowserbase) -> None: + async def test_method_execute_agent_with_all_params(self, async_client: AsyncStagehand) -> None: session = await async_client.sessions.execute_agent( session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", agent_config={ @@ -588,7 +588,7 @@ async def test_method_execute_agent_with_all_params(self, async_client: AsyncBro @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - async def test_raw_response_execute_agent(self, async_client: AsyncBrowserbase) -> None: + async def test_raw_response_execute_agent(self, async_client: AsyncStagehand) -> None: response = await async_client.sessions.with_raw_response.execute_agent( session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", agent_config={}, @@ -602,7 +602,7 @@ async def test_raw_response_execute_agent(self, async_client: AsyncBrowserbase) @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - async def test_streaming_response_execute_agent(self, async_client: AsyncBrowserbase) -> None: + async def test_streaming_response_execute_agent(self, async_client: AsyncStagehand) -> None: async with async_client.sessions.with_streaming_response.execute_agent( session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", agent_config={}, @@ -618,7 +618,7 @@ async def test_streaming_response_execute_agent(self, async_client: AsyncBrowser @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - async def test_path_params_execute_agent(self, async_client: AsyncBrowserbase) -> None: + async def test_path_params_execute_agent(self, async_client: AsyncStagehand) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `session_id` but received ''"): await async_client.sessions.with_raw_response.execute_agent( session_id="", @@ -628,7 +628,7 @@ async def test_path_params_execute_agent(self, async_client: AsyncBrowserbase) - @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - async def test_method_extract(self, async_client: AsyncBrowserbase) -> None: + async def test_method_extract(self, async_client: AsyncStagehand) -> None: session = await async_client.sessions.extract( session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", ) @@ -636,7 +636,7 @@ async def test_method_extract(self, async_client: AsyncBrowserbase) -> None: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - async def test_method_extract_with_all_params(self, async_client: AsyncBrowserbase) -> None: + async def test_method_extract_with_all_params(self, async_client: AsyncStagehand) -> None: session = await async_client.sessions.extract( session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", frame_id="frameId", @@ -658,7 +658,7 @@ async def test_method_extract_with_all_params(self, async_client: AsyncBrowserba @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - async def test_raw_response_extract(self, async_client: AsyncBrowserbase) -> None: + async def test_raw_response_extract(self, async_client: AsyncStagehand) -> None: response = await async_client.sessions.with_raw_response.extract( session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", ) @@ -670,7 +670,7 @@ async def test_raw_response_extract(self, async_client: AsyncBrowserbase) -> Non @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - async def test_streaming_response_extract(self, async_client: AsyncBrowserbase) -> None: + async def test_streaming_response_extract(self, async_client: AsyncStagehand) -> None: async with async_client.sessions.with_streaming_response.extract( session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", ) as response: @@ -684,7 +684,7 @@ async def test_streaming_response_extract(self, async_client: AsyncBrowserbase) @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - async def test_path_params_extract(self, async_client: AsyncBrowserbase) -> None: + async def test_path_params_extract(self, async_client: AsyncStagehand) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `session_id` but received ''"): await async_client.sessions.with_raw_response.extract( session_id="", @@ -692,7 +692,7 @@ async def test_path_params_extract(self, async_client: AsyncBrowserbase) -> None @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - async def test_method_navigate(self, async_client: AsyncBrowserbase) -> None: + async def test_method_navigate(self, async_client: AsyncStagehand) -> None: session = await async_client.sessions.navigate( session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", url="https://example.com", @@ -701,7 +701,7 @@ async def test_method_navigate(self, async_client: AsyncBrowserbase) -> None: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - async def test_method_navigate_with_all_params(self, async_client: AsyncBrowserbase) -> None: + async def test_method_navigate_with_all_params(self, async_client: AsyncStagehand) -> None: session = await async_client.sessions.navigate( session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", url="https://example.com", @@ -713,7 +713,7 @@ async def test_method_navigate_with_all_params(self, async_client: AsyncBrowserb @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - async def test_raw_response_navigate(self, async_client: AsyncBrowserbase) -> None: + async def test_raw_response_navigate(self, async_client: AsyncStagehand) -> None: response = await async_client.sessions.with_raw_response.navigate( session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", url="https://example.com", @@ -726,7 +726,7 @@ async def test_raw_response_navigate(self, async_client: AsyncBrowserbase) -> No @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - async def test_streaming_response_navigate(self, async_client: AsyncBrowserbase) -> None: + async def test_streaming_response_navigate(self, async_client: AsyncStagehand) -> None: async with async_client.sessions.with_streaming_response.navigate( session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", url="https://example.com", @@ -741,7 +741,7 @@ async def test_streaming_response_navigate(self, async_client: AsyncBrowserbase) @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - async def test_path_params_navigate(self, async_client: AsyncBrowserbase) -> None: + async def test_path_params_navigate(self, async_client: AsyncStagehand) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `session_id` but received ''"): await async_client.sessions.with_raw_response.navigate( session_id="", @@ -750,7 +750,7 @@ async def test_path_params_navigate(self, async_client: AsyncBrowserbase) -> Non @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - async def test_method_observe(self, async_client: AsyncBrowserbase) -> None: + async def test_method_observe(self, async_client: AsyncStagehand) -> None: session = await async_client.sessions.observe( session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", ) @@ -758,7 +758,7 @@ async def test_method_observe(self, async_client: AsyncBrowserbase) -> None: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - async def test_method_observe_with_all_params(self, async_client: AsyncBrowserbase) -> None: + async def test_method_observe_with_all_params(self, async_client: AsyncStagehand) -> None: session = await async_client.sessions.observe( session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", frame_id="frameId", @@ -779,7 +779,7 @@ async def test_method_observe_with_all_params(self, async_client: AsyncBrowserba @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - async def test_raw_response_observe(self, async_client: AsyncBrowserbase) -> None: + async def test_raw_response_observe(self, async_client: AsyncStagehand) -> None: response = await async_client.sessions.with_raw_response.observe( session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", ) @@ -791,7 +791,7 @@ async def test_raw_response_observe(self, async_client: AsyncBrowserbase) -> Non @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - async def test_streaming_response_observe(self, async_client: AsyncBrowserbase) -> None: + async def test_streaming_response_observe(self, async_client: AsyncStagehand) -> None: async with async_client.sessions.with_streaming_response.observe( session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", ) as response: @@ -805,7 +805,7 @@ async def test_streaming_response_observe(self, async_client: AsyncBrowserbase) @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - async def test_path_params_observe(self, async_client: AsyncBrowserbase) -> None: + async def test_path_params_observe(self, async_client: AsyncStagehand) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `session_id` but received ''"): await async_client.sessions.with_raw_response.observe( session_id="", @@ -813,7 +813,7 @@ async def test_path_params_observe(self, async_client: AsyncBrowserbase) -> None @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - async def test_method_start(self, async_client: AsyncBrowserbase) -> None: + async def test_method_start(self, async_client: AsyncStagehand) -> None: session = await async_client.sessions.start( env="LOCAL", ) @@ -821,7 +821,7 @@ async def test_method_start(self, async_client: AsyncBrowserbase) -> None: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - async def test_method_start_with_all_params(self, async_client: AsyncBrowserbase) -> None: + async def test_method_start_with_all_params(self, async_client: AsyncStagehand) -> None: session = await async_client.sessions.start( env="LOCAL", api_key="apiKey", @@ -837,7 +837,7 @@ async def test_method_start_with_all_params(self, async_client: AsyncBrowserbase @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - async def test_raw_response_start(self, async_client: AsyncBrowserbase) -> None: + async def test_raw_response_start(self, async_client: AsyncStagehand) -> None: response = await async_client.sessions.with_raw_response.start( env="LOCAL", ) @@ -849,7 +849,7 @@ async def test_raw_response_start(self, async_client: AsyncBrowserbase) -> None: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - async def test_streaming_response_start(self, async_client: AsyncBrowserbase) -> None: + async def test_streaming_response_start(self, async_client: AsyncStagehand) -> None: async with async_client.sessions.with_streaming_response.start( env="LOCAL", ) as response: diff --git a/tests/conftest.py b/tests/conftest.py index 1d6ab407..f57b7286 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -10,7 +10,7 @@ import pytest from pytest_asyncio import is_async_test -from stagehand import Browserbase, AsyncBrowserbase, DefaultAioHttpClient +from stagehand import Stagehand, AsyncStagehand, DefaultAioHttpClient from stagehand._utils import is_dict if TYPE_CHECKING: @@ -49,17 +49,17 @@ def pytest_collection_modifyitems(items: list[pytest.Function]) -> None: @pytest.fixture(scope="session") -def client(request: FixtureRequest) -> Iterator[Browserbase]: +def client(request: FixtureRequest) -> Iterator[Stagehand]: strict = getattr(request, "param", True) if not isinstance(strict, bool): raise TypeError(f"Unexpected fixture parameter type {type(strict)}, expected {bool}") - with Browserbase(base_url=base_url, api_key=api_key, _strict_response_validation=strict) as client: + with Stagehand(base_url=base_url, api_key=api_key, _strict_response_validation=strict) as client: yield client @pytest.fixture(scope="session") -async def async_client(request: FixtureRequest) -> AsyncIterator[AsyncBrowserbase]: +async def async_client(request: FixtureRequest) -> AsyncIterator[AsyncStagehand]: param = getattr(request, "param", True) # defaults @@ -78,7 +78,7 @@ async def async_client(request: FixtureRequest) -> AsyncIterator[AsyncBrowserbas else: raise TypeError(f"Unexpected fixture parameter type {type(param)}, expected bool or dict") - async with AsyncBrowserbase( + async with AsyncStagehand( base_url=base_url, api_key=api_key, _strict_response_validation=strict, http_client=http_client ) as client: yield client diff --git a/tests/test_client.py b/tests/test_client.py index b490ea9f..fec791da 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -18,11 +18,11 @@ from respx import MockRouter from pydantic import ValidationError -from stagehand import Browserbase, AsyncBrowserbase, APIResponseValidationError +from stagehand import Stagehand, AsyncStagehand, APIResponseValidationError from stagehand._types import Omit from stagehand._utils import asyncify from stagehand._models import BaseModel, FinalRequestOptions -from stagehand._exceptions import APIStatusError, APITimeoutError, BrowserbaseError, APIResponseValidationError +from stagehand._exceptions import APIStatusError, StagehandError, APITimeoutError, APIResponseValidationError from stagehand._base_client import ( DEFAULT_TIMEOUT, HTTPX_DEFAULT_TIMEOUT, @@ -50,7 +50,7 @@ def _low_retry_timeout(*_args: Any, **_kwargs: Any) -> float: return 0.1 -def _get_open_connections(client: Browserbase | AsyncBrowserbase) -> int: +def _get_open_connections(client: Stagehand | AsyncStagehand) -> int: transport = client._client._transport assert isinstance(transport, httpx.HTTPTransport) or isinstance(transport, httpx.AsyncHTTPTransport) @@ -58,9 +58,9 @@ def _get_open_connections(client: Browserbase | AsyncBrowserbase) -> int: return len(pool._requests) -class TestBrowserbase: +class TestStagehand: @pytest.mark.respx(base_url=base_url) - def test_raw_response(self, respx_mock: MockRouter, client: Browserbase) -> None: + def test_raw_response(self, respx_mock: MockRouter, client: Stagehand) -> None: respx_mock.post("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"})) response = client.post("/foo", cast_to=httpx.Response) @@ -69,7 +69,7 @@ def test_raw_response(self, respx_mock: MockRouter, client: Browserbase) -> None assert response.json() == {"foo": "bar"} @pytest.mark.respx(base_url=base_url) - def test_raw_response_for_binary(self, respx_mock: MockRouter, client: Browserbase) -> None: + def test_raw_response_for_binary(self, respx_mock: MockRouter, client: Stagehand) -> None: respx_mock.post("/foo").mock( return_value=httpx.Response(200, headers={"Content-Type": "application/binary"}, content='{"foo": "bar"}') ) @@ -79,7 +79,7 @@ def test_raw_response_for_binary(self, respx_mock: MockRouter, client: Browserba assert isinstance(response, httpx.Response) assert response.json() == {"foo": "bar"} - def test_copy(self, client: Browserbase) -> None: + def test_copy(self, client: Stagehand) -> None: copied = client.copy() assert id(copied) != id(client) @@ -87,7 +87,7 @@ def test_copy(self, client: Browserbase) -> None: assert copied.api_key == "another My API Key" assert client.api_key == "My API Key" - def test_copy_default_options(self, client: Browserbase) -> None: + def test_copy_default_options(self, client: Stagehand) -> None: # options that have a default are overridden correctly copied = client.copy(max_retries=7) assert copied.max_retries == 7 @@ -104,7 +104,7 @@ def test_copy_default_options(self, client: Browserbase) -> None: assert isinstance(client.timeout, httpx.Timeout) def test_copy_default_headers(self) -> None: - client = Browserbase( + client = Stagehand( base_url=base_url, api_key=api_key, _strict_response_validation=True, default_headers={"X-Foo": "bar"} ) assert client.default_headers["X-Foo"] == "bar" @@ -139,7 +139,7 @@ def test_copy_default_headers(self) -> None: client.close() def test_copy_default_query(self) -> None: - client = Browserbase( + client = Stagehand( base_url=base_url, api_key=api_key, _strict_response_validation=True, default_query={"foo": "bar"} ) assert _get_params(client)["foo"] == "bar" @@ -176,7 +176,7 @@ def test_copy_default_query(self) -> None: client.close() - def test_copy_signature(self, client: Browserbase) -> None: + def test_copy_signature(self, client: Stagehand) -> None: # ensure the same parameters that can be passed to the client are defined in the `.copy()` method init_signature = inspect.signature( # mypy doesn't like that we access the `__init__` property. @@ -193,7 +193,7 @@ def test_copy_signature(self, client: Browserbase) -> None: assert copy_param is not None, f"copy() signature is missing the {name} param" @pytest.mark.skipif(sys.version_info >= (3, 10), reason="fails because of a memory leak that started from 3.12") - def test_copy_build_request(self, client: Browserbase) -> None: + def test_copy_build_request(self, client: Stagehand) -> None: options = FinalRequestOptions(method="get", url="/foo") def build_request(options: FinalRequestOptions) -> None: @@ -255,7 +255,7 @@ def add_leak(leaks: list[tracemalloc.StatisticDiff], diff: tracemalloc.Statistic print(frame) raise AssertionError() - def test_request_timeout(self, client: Browserbase) -> None: + def test_request_timeout(self, client: Stagehand) -> None: request = client._build_request(FinalRequestOptions(method="get", url="/foo")) timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore assert timeout == DEFAULT_TIMEOUT @@ -265,7 +265,7 @@ def test_request_timeout(self, client: Browserbase) -> None: assert timeout == httpx.Timeout(100.0) def test_client_timeout_option(self) -> None: - client = Browserbase( + client = Stagehand( base_url=base_url, api_key=api_key, _strict_response_validation=True, timeout=httpx.Timeout(0) ) @@ -278,7 +278,7 @@ def test_client_timeout_option(self) -> None: def test_http_client_timeout_option(self) -> None: # custom timeout given to the httpx client should be used with httpx.Client(timeout=None) as http_client: - client = Browserbase( + client = Stagehand( base_url=base_url, api_key=api_key, _strict_response_validation=True, http_client=http_client ) @@ -290,7 +290,7 @@ def test_http_client_timeout_option(self) -> None: # no timeout given to the httpx client should not use the httpx default with httpx.Client() as http_client: - client = Browserbase( + client = Stagehand( base_url=base_url, api_key=api_key, _strict_response_validation=True, http_client=http_client ) @@ -302,7 +302,7 @@ def test_http_client_timeout_option(self) -> None: # explicitly passing the default timeout currently results in it being ignored with httpx.Client(timeout=HTTPX_DEFAULT_TIMEOUT) as http_client: - client = Browserbase( + client = Stagehand( base_url=base_url, api_key=api_key, _strict_response_validation=True, http_client=http_client ) @@ -315,7 +315,7 @@ def test_http_client_timeout_option(self) -> None: async def test_invalid_http_client(self) -> None: with pytest.raises(TypeError, match="Invalid `http_client` arg"): async with httpx.AsyncClient() as http_client: - Browserbase( + Stagehand( base_url=base_url, api_key=api_key, _strict_response_validation=True, @@ -323,14 +323,14 @@ async def test_invalid_http_client(self) -> None: ) def test_default_headers_option(self) -> None: - test_client = Browserbase( + test_client = Stagehand( base_url=base_url, api_key=api_key, _strict_response_validation=True, default_headers={"X-Foo": "bar"} ) request = test_client._build_request(FinalRequestOptions(method="get", url="/foo")) assert request.headers.get("x-foo") == "bar" assert request.headers.get("x-stainless-lang") == "python" - test_client2 = Browserbase( + test_client2 = Stagehand( base_url=base_url, api_key=api_key, _strict_response_validation=True, @@ -347,17 +347,17 @@ def test_default_headers_option(self) -> None: test_client2.close() def test_validate_headers(self) -> None: - client = Browserbase(base_url=base_url, api_key=api_key, _strict_response_validation=True) + client = Stagehand(base_url=base_url, api_key=api_key, _strict_response_validation=True) request = client._build_request(FinalRequestOptions(method="get", url="/foo")) assert request.headers.get("Authorization") == f"Bearer {api_key}" - with pytest.raises(BrowserbaseError): + with pytest.raises(StagehandError): with update_env(**{"STAGEHAND_API_KEY": Omit()}): - client2 = Browserbase(base_url=base_url, api_key=None, _strict_response_validation=True) + client2 = Stagehand(base_url=base_url, api_key=None, _strict_response_validation=True) _ = client2 def test_default_query_option(self) -> None: - client = Browserbase( + client = Stagehand( base_url=base_url, api_key=api_key, _strict_response_validation=True, default_query={"query_param": "bar"} ) request = client._build_request(FinalRequestOptions(method="get", url="/foo")) @@ -376,7 +376,7 @@ def test_default_query_option(self) -> None: client.close() - def test_request_extra_json(self, client: Browserbase) -> None: + def test_request_extra_json(self, client: Stagehand) -> None: request = client._build_request( FinalRequestOptions( method="post", @@ -410,7 +410,7 @@ def test_request_extra_json(self, client: Browserbase) -> None: data = json.loads(request.content.decode("utf-8")) assert data == {"foo": "bar", "baz": None} - def test_request_extra_headers(self, client: Browserbase) -> None: + def test_request_extra_headers(self, client: Stagehand) -> None: request = client._build_request( FinalRequestOptions( method="post", @@ -432,7 +432,7 @@ def test_request_extra_headers(self, client: Browserbase) -> None: ) assert request.headers.get("X-Bar") == "false" - def test_request_extra_query(self, client: Browserbase) -> None: + def test_request_extra_query(self, client: Stagehand) -> None: request = client._build_request( FinalRequestOptions( method="post", @@ -473,7 +473,7 @@ def test_request_extra_query(self, client: Browserbase) -> None: params = dict(request.url.params) assert params == {"foo": "2"} - def test_multipart_repeating_array(self, client: Browserbase) -> None: + def test_multipart_repeating_array(self, client: Stagehand) -> None: request = client._build_request( FinalRequestOptions.construct( method="post", @@ -503,7 +503,7 @@ def test_multipart_repeating_array(self, client: Browserbase) -> None: ] @pytest.mark.respx(base_url=base_url) - def test_basic_union_response(self, respx_mock: MockRouter, client: Browserbase) -> None: + def test_basic_union_response(self, respx_mock: MockRouter, client: Stagehand) -> None: class Model1(BaseModel): name: str @@ -517,7 +517,7 @@ class Model2(BaseModel): assert response.foo == "bar" @pytest.mark.respx(base_url=base_url) - def test_union_response_different_types(self, respx_mock: MockRouter, client: Browserbase) -> None: + def test_union_response_different_types(self, respx_mock: MockRouter, client: Stagehand) -> None: """Union of objects with the same field name using a different type""" class Model1(BaseModel): @@ -539,7 +539,7 @@ class Model2(BaseModel): assert response.foo == 1 @pytest.mark.respx(base_url=base_url) - def test_non_application_json_content_type_for_json_data(self, respx_mock: MockRouter, client: Browserbase) -> None: + def test_non_application_json_content_type_for_json_data(self, respx_mock: MockRouter, client: Stagehand) -> None: """ Response that sets Content-Type to something other than application/json but returns json data """ @@ -560,9 +560,7 @@ class Model(BaseModel): assert response.foo == 2 def test_base_url_setter(self) -> None: - client = Browserbase( - base_url="https://example.com/from_init", api_key=api_key, _strict_response_validation=True - ) + client = Stagehand(base_url="https://example.com/from_init", api_key=api_key, _strict_response_validation=True) assert client.base_url == "https://example.com/from_init/" client.base_url = "https://example.com/from_setter" # type: ignore[assignment] @@ -572,16 +570,16 @@ def test_base_url_setter(self) -> None: client.close() def test_base_url_env(self) -> None: - with update_env(BROWSERBASE_BASE_URL="http://localhost:5000/from/env"): - client = Browserbase(api_key=api_key, _strict_response_validation=True) + with update_env(STAGEHAND_BASE_URL="http://localhost:5000/from/env"): + client = Stagehand(api_key=api_key, _strict_response_validation=True) assert client.base_url == "http://localhost:5000/from/env/" # explicit environment arg requires explicitness - with update_env(BROWSERBASE_BASE_URL="http://localhost:5000/from/env"): + with update_env(STAGEHAND_BASE_URL="http://localhost:5000/from/env"): with pytest.raises(ValueError, match=r"you must pass base_url=None"): - Browserbase(api_key=api_key, _strict_response_validation=True, environment="production") + Stagehand(api_key=api_key, _strict_response_validation=True, environment="production") - client = Browserbase( + client = Stagehand( base_url=None, api_key=api_key, _strict_response_validation=True, environment="production" ) assert str(client.base_url).startswith("https://api.stagehand.browserbase.com/v1") @@ -591,10 +589,8 @@ def test_base_url_env(self) -> None: @pytest.mark.parametrize( "client", [ - Browserbase( - base_url="http://localhost:5000/custom/path/", api_key=api_key, _strict_response_validation=True - ), - Browserbase( + Stagehand(base_url="http://localhost:5000/custom/path/", api_key=api_key, _strict_response_validation=True), + Stagehand( base_url="http://localhost:5000/custom/path/", api_key=api_key, _strict_response_validation=True, @@ -603,7 +599,7 @@ def test_base_url_env(self) -> None: ], ids=["standard", "custom http client"], ) - def test_base_url_trailing_slash(self, client: Browserbase) -> None: + def test_base_url_trailing_slash(self, client: Stagehand) -> None: request = client._build_request( FinalRequestOptions( method="post", @@ -617,10 +613,8 @@ def test_base_url_trailing_slash(self, client: Browserbase) -> None: @pytest.mark.parametrize( "client", [ - Browserbase( - base_url="http://localhost:5000/custom/path/", api_key=api_key, _strict_response_validation=True - ), - Browserbase( + Stagehand(base_url="http://localhost:5000/custom/path/", api_key=api_key, _strict_response_validation=True), + Stagehand( base_url="http://localhost:5000/custom/path/", api_key=api_key, _strict_response_validation=True, @@ -629,7 +623,7 @@ def test_base_url_trailing_slash(self, client: Browserbase) -> None: ], ids=["standard", "custom http client"], ) - def test_base_url_no_trailing_slash(self, client: Browserbase) -> None: + def test_base_url_no_trailing_slash(self, client: Stagehand) -> None: request = client._build_request( FinalRequestOptions( method="post", @@ -643,10 +637,8 @@ def test_base_url_no_trailing_slash(self, client: Browserbase) -> None: @pytest.mark.parametrize( "client", [ - Browserbase( - base_url="http://localhost:5000/custom/path/", api_key=api_key, _strict_response_validation=True - ), - Browserbase( + Stagehand(base_url="http://localhost:5000/custom/path/", api_key=api_key, _strict_response_validation=True), + Stagehand( base_url="http://localhost:5000/custom/path/", api_key=api_key, _strict_response_validation=True, @@ -655,7 +647,7 @@ def test_base_url_no_trailing_slash(self, client: Browserbase) -> None: ], ids=["standard", "custom http client"], ) - def test_absolute_request_url(self, client: Browserbase) -> None: + def test_absolute_request_url(self, client: Stagehand) -> None: request = client._build_request( FinalRequestOptions( method="post", @@ -667,7 +659,7 @@ def test_absolute_request_url(self, client: Browserbase) -> None: client.close() def test_copied_client_does_not_close_http(self) -> None: - test_client = Browserbase(base_url=base_url, api_key=api_key, _strict_response_validation=True) + test_client = Stagehand(base_url=base_url, api_key=api_key, _strict_response_validation=True) assert not test_client.is_closed() copied = test_client.copy() @@ -678,7 +670,7 @@ def test_copied_client_does_not_close_http(self) -> None: assert not test_client.is_closed() def test_client_context_manager(self) -> None: - test_client = Browserbase(base_url=base_url, api_key=api_key, _strict_response_validation=True) + test_client = Stagehand(base_url=base_url, api_key=api_key, _strict_response_validation=True) with test_client as c2: assert c2 is test_client assert not c2.is_closed() @@ -686,7 +678,7 @@ def test_client_context_manager(self) -> None: assert test_client.is_closed() @pytest.mark.respx(base_url=base_url) - def test_client_response_validation_error(self, respx_mock: MockRouter, client: Browserbase) -> None: + def test_client_response_validation_error(self, respx_mock: MockRouter, client: Stagehand) -> None: class Model(BaseModel): foo: str @@ -699,9 +691,7 @@ class Model(BaseModel): def test_client_max_retries_validation(self) -> None: with pytest.raises(TypeError, match=r"max_retries cannot be None"): - Browserbase( - base_url=base_url, api_key=api_key, _strict_response_validation=True, max_retries=cast(Any, None) - ) + Stagehand(base_url=base_url, api_key=api_key, _strict_response_validation=True, max_retries=cast(Any, None)) @pytest.mark.respx(base_url=base_url) def test_received_text_for_expected_json(self, respx_mock: MockRouter) -> None: @@ -710,12 +700,12 @@ class Model(BaseModel): respx_mock.get("/foo").mock(return_value=httpx.Response(200, text="my-custom-format")) - strict_client = Browserbase(base_url=base_url, api_key=api_key, _strict_response_validation=True) + strict_client = Stagehand(base_url=base_url, api_key=api_key, _strict_response_validation=True) with pytest.raises(APIResponseValidationError): strict_client.get("/foo", cast_to=Model) - non_strict_client = Browserbase(base_url=base_url, api_key=api_key, _strict_response_validation=False) + non_strict_client = Stagehand(base_url=base_url, api_key=api_key, _strict_response_validation=False) response = non_strict_client.get("/foo", cast_to=Model) assert isinstance(response, str) # type: ignore[unreachable] @@ -746,7 +736,7 @@ class Model(BaseModel): ) @mock.patch("time.time", mock.MagicMock(return_value=1696004797)) def test_parse_retry_after_header( - self, remaining_retries: int, retry_after: str, timeout: float, client: Browserbase + self, remaining_retries: int, retry_after: str, timeout: float, client: Stagehand ) -> None: headers = httpx.Headers({"retry-after": retry_after}) options = FinalRequestOptions(method="get", url="/foo", max_retries=3) @@ -755,7 +745,7 @@ def test_parse_retry_after_header( @mock.patch("stagehand._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) @pytest.mark.respx(base_url=base_url) - def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter, client: Browserbase) -> None: + def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter, client: Stagehand) -> None: respx_mock.post("/sessions/start").mock(side_effect=httpx.TimeoutException("Test timeout error")) with pytest.raises(APITimeoutError): @@ -765,7 +755,7 @@ def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter, clien @mock.patch("stagehand._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) @pytest.mark.respx(base_url=base_url) - def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter, client: Browserbase) -> None: + def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter, client: Stagehand) -> None: respx_mock.post("/sessions/start").mock(return_value=httpx.Response(500)) with pytest.raises(APIStatusError): @@ -778,7 +768,7 @@ def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter, client @pytest.mark.parametrize("failure_mode", ["status", "exception"]) def test_retries_taken( self, - client: Browserbase, + client: Stagehand, failures_before_success: int, failure_mode: Literal["status", "exception"], respx_mock: MockRouter, @@ -807,7 +797,7 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: @mock.patch("stagehand._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) @pytest.mark.respx(base_url=base_url) def test_omit_retry_count_header( - self, client: Browserbase, failures_before_success: int, respx_mock: MockRouter + self, client: Stagehand, failures_before_success: int, respx_mock: MockRouter ) -> None: client = client.with_options(max_retries=4) @@ -832,7 +822,7 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: @mock.patch("stagehand._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) @pytest.mark.respx(base_url=base_url) def test_overwrite_retry_count_header( - self, client: Browserbase, failures_before_success: int, respx_mock: MockRouter + self, client: Stagehand, failures_before_success: int, respx_mock: MockRouter ) -> None: client = client.with_options(max_retries=4) @@ -874,7 +864,7 @@ def test_default_client_creation(self) -> None: ) @pytest.mark.respx(base_url=base_url) - def test_follow_redirects(self, respx_mock: MockRouter, client: Browserbase) -> None: + def test_follow_redirects(self, respx_mock: MockRouter, client: Stagehand) -> None: # Test that the default follow_redirects=True allows following redirects respx_mock.post("/redirect").mock( return_value=httpx.Response(302, headers={"Location": f"{base_url}/redirected"}) @@ -886,7 +876,7 @@ def test_follow_redirects(self, respx_mock: MockRouter, client: Browserbase) -> assert response.json() == {"status": "ok"} @pytest.mark.respx(base_url=base_url) - def test_follow_redirects_disabled(self, respx_mock: MockRouter, client: Browserbase) -> None: + def test_follow_redirects_disabled(self, respx_mock: MockRouter, client: Stagehand) -> None: # Test that follow_redirects=False prevents following redirects respx_mock.post("/redirect").mock( return_value=httpx.Response(302, headers={"Location": f"{base_url}/redirected"}) @@ -899,9 +889,9 @@ def test_follow_redirects_disabled(self, respx_mock: MockRouter, client: Browser assert exc_info.value.response.headers["Location"] == f"{base_url}/redirected" -class TestAsyncBrowserbase: +class TestAsyncStagehand: @pytest.mark.respx(base_url=base_url) - async def test_raw_response(self, respx_mock: MockRouter, async_client: AsyncBrowserbase) -> None: + async def test_raw_response(self, respx_mock: MockRouter, async_client: AsyncStagehand) -> None: respx_mock.post("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"})) response = await async_client.post("/foo", cast_to=httpx.Response) @@ -910,7 +900,7 @@ async def test_raw_response(self, respx_mock: MockRouter, async_client: AsyncBro assert response.json() == {"foo": "bar"} @pytest.mark.respx(base_url=base_url) - async def test_raw_response_for_binary(self, respx_mock: MockRouter, async_client: AsyncBrowserbase) -> None: + async def test_raw_response_for_binary(self, respx_mock: MockRouter, async_client: AsyncStagehand) -> None: respx_mock.post("/foo").mock( return_value=httpx.Response(200, headers={"Content-Type": "application/binary"}, content='{"foo": "bar"}') ) @@ -920,7 +910,7 @@ async def test_raw_response_for_binary(self, respx_mock: MockRouter, async_clien assert isinstance(response, httpx.Response) assert response.json() == {"foo": "bar"} - def test_copy(self, async_client: AsyncBrowserbase) -> None: + def test_copy(self, async_client: AsyncStagehand) -> None: copied = async_client.copy() assert id(copied) != id(async_client) @@ -928,7 +918,7 @@ def test_copy(self, async_client: AsyncBrowserbase) -> None: assert copied.api_key == "another My API Key" assert async_client.api_key == "My API Key" - def test_copy_default_options(self, async_client: AsyncBrowserbase) -> None: + def test_copy_default_options(self, async_client: AsyncStagehand) -> None: # options that have a default are overridden correctly copied = async_client.copy(max_retries=7) assert copied.max_retries == 7 @@ -945,7 +935,7 @@ def test_copy_default_options(self, async_client: AsyncBrowserbase) -> None: assert isinstance(async_client.timeout, httpx.Timeout) async def test_copy_default_headers(self) -> None: - client = AsyncBrowserbase( + client = AsyncStagehand( base_url=base_url, api_key=api_key, _strict_response_validation=True, default_headers={"X-Foo": "bar"} ) assert client.default_headers["X-Foo"] == "bar" @@ -980,7 +970,7 @@ async def test_copy_default_headers(self) -> None: await client.close() async def test_copy_default_query(self) -> None: - client = AsyncBrowserbase( + client = AsyncStagehand( base_url=base_url, api_key=api_key, _strict_response_validation=True, default_query={"foo": "bar"} ) assert _get_params(client)["foo"] == "bar" @@ -1017,7 +1007,7 @@ async def test_copy_default_query(self) -> None: await client.close() - def test_copy_signature(self, async_client: AsyncBrowserbase) -> None: + def test_copy_signature(self, async_client: AsyncStagehand) -> None: # ensure the same parameters that can be passed to the client are defined in the `.copy()` method init_signature = inspect.signature( # mypy doesn't like that we access the `__init__` property. @@ -1034,7 +1024,7 @@ def test_copy_signature(self, async_client: AsyncBrowserbase) -> None: assert copy_param is not None, f"copy() signature is missing the {name} param" @pytest.mark.skipif(sys.version_info >= (3, 10), reason="fails because of a memory leak that started from 3.12") - def test_copy_build_request(self, async_client: AsyncBrowserbase) -> None: + def test_copy_build_request(self, async_client: AsyncStagehand) -> None: options = FinalRequestOptions(method="get", url="/foo") def build_request(options: FinalRequestOptions) -> None: @@ -1096,7 +1086,7 @@ def add_leak(leaks: list[tracemalloc.StatisticDiff], diff: tracemalloc.Statistic print(frame) raise AssertionError() - async def test_request_timeout(self, async_client: AsyncBrowserbase) -> None: + async def test_request_timeout(self, async_client: AsyncStagehand) -> None: request = async_client._build_request(FinalRequestOptions(method="get", url="/foo")) timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore assert timeout == DEFAULT_TIMEOUT @@ -1108,7 +1098,7 @@ async def test_request_timeout(self, async_client: AsyncBrowserbase) -> None: assert timeout == httpx.Timeout(100.0) async def test_client_timeout_option(self) -> None: - client = AsyncBrowserbase( + client = AsyncStagehand( base_url=base_url, api_key=api_key, _strict_response_validation=True, timeout=httpx.Timeout(0) ) @@ -1121,7 +1111,7 @@ async def test_client_timeout_option(self) -> None: async def test_http_client_timeout_option(self) -> None: # custom timeout given to the httpx client should be used async with httpx.AsyncClient(timeout=None) as http_client: - client = AsyncBrowserbase( + client = AsyncStagehand( base_url=base_url, api_key=api_key, _strict_response_validation=True, http_client=http_client ) @@ -1133,7 +1123,7 @@ async def test_http_client_timeout_option(self) -> None: # no timeout given to the httpx client should not use the httpx default async with httpx.AsyncClient() as http_client: - client = AsyncBrowserbase( + client = AsyncStagehand( base_url=base_url, api_key=api_key, _strict_response_validation=True, http_client=http_client ) @@ -1145,7 +1135,7 @@ async def test_http_client_timeout_option(self) -> None: # explicitly passing the default timeout currently results in it being ignored async with httpx.AsyncClient(timeout=HTTPX_DEFAULT_TIMEOUT) as http_client: - client = AsyncBrowserbase( + client = AsyncStagehand( base_url=base_url, api_key=api_key, _strict_response_validation=True, http_client=http_client ) @@ -1158,7 +1148,7 @@ async def test_http_client_timeout_option(self) -> None: def test_invalid_http_client(self) -> None: with pytest.raises(TypeError, match="Invalid `http_client` arg"): with httpx.Client() as http_client: - AsyncBrowserbase( + AsyncStagehand( base_url=base_url, api_key=api_key, _strict_response_validation=True, @@ -1166,14 +1156,14 @@ def test_invalid_http_client(self) -> None: ) async def test_default_headers_option(self) -> None: - test_client = AsyncBrowserbase( + test_client = AsyncStagehand( base_url=base_url, api_key=api_key, _strict_response_validation=True, default_headers={"X-Foo": "bar"} ) request = test_client._build_request(FinalRequestOptions(method="get", url="/foo")) assert request.headers.get("x-foo") == "bar" assert request.headers.get("x-stainless-lang") == "python" - test_client2 = AsyncBrowserbase( + test_client2 = AsyncStagehand( base_url=base_url, api_key=api_key, _strict_response_validation=True, @@ -1190,17 +1180,17 @@ async def test_default_headers_option(self) -> None: await test_client2.close() def test_validate_headers(self) -> None: - client = AsyncBrowserbase(base_url=base_url, api_key=api_key, _strict_response_validation=True) + client = AsyncStagehand(base_url=base_url, api_key=api_key, _strict_response_validation=True) request = client._build_request(FinalRequestOptions(method="get", url="/foo")) assert request.headers.get("Authorization") == f"Bearer {api_key}" - with pytest.raises(BrowserbaseError): + with pytest.raises(StagehandError): with update_env(**{"STAGEHAND_API_KEY": Omit()}): - client2 = AsyncBrowserbase(base_url=base_url, api_key=None, _strict_response_validation=True) + client2 = AsyncStagehand(base_url=base_url, api_key=None, _strict_response_validation=True) _ = client2 async def test_default_query_option(self) -> None: - client = AsyncBrowserbase( + client = AsyncStagehand( base_url=base_url, api_key=api_key, _strict_response_validation=True, default_query={"query_param": "bar"} ) request = client._build_request(FinalRequestOptions(method="get", url="/foo")) @@ -1219,7 +1209,7 @@ async def test_default_query_option(self) -> None: await client.close() - def test_request_extra_json(self, client: Browserbase) -> None: + def test_request_extra_json(self, client: Stagehand) -> None: request = client._build_request( FinalRequestOptions( method="post", @@ -1253,7 +1243,7 @@ def test_request_extra_json(self, client: Browserbase) -> None: data = json.loads(request.content.decode("utf-8")) assert data == {"foo": "bar", "baz": None} - def test_request_extra_headers(self, client: Browserbase) -> None: + def test_request_extra_headers(self, client: Stagehand) -> None: request = client._build_request( FinalRequestOptions( method="post", @@ -1275,7 +1265,7 @@ def test_request_extra_headers(self, client: Browserbase) -> None: ) assert request.headers.get("X-Bar") == "false" - def test_request_extra_query(self, client: Browserbase) -> None: + def test_request_extra_query(self, client: Stagehand) -> None: request = client._build_request( FinalRequestOptions( method="post", @@ -1316,7 +1306,7 @@ def test_request_extra_query(self, client: Browserbase) -> None: params = dict(request.url.params) assert params == {"foo": "2"} - def test_multipart_repeating_array(self, async_client: AsyncBrowserbase) -> None: + def test_multipart_repeating_array(self, async_client: AsyncStagehand) -> None: request = async_client._build_request( FinalRequestOptions.construct( method="post", @@ -1346,7 +1336,7 @@ def test_multipart_repeating_array(self, async_client: AsyncBrowserbase) -> None ] @pytest.mark.respx(base_url=base_url) - async def test_basic_union_response(self, respx_mock: MockRouter, async_client: AsyncBrowserbase) -> None: + async def test_basic_union_response(self, respx_mock: MockRouter, async_client: AsyncStagehand) -> None: class Model1(BaseModel): name: str @@ -1360,7 +1350,7 @@ class Model2(BaseModel): assert response.foo == "bar" @pytest.mark.respx(base_url=base_url) - async def test_union_response_different_types(self, respx_mock: MockRouter, async_client: AsyncBrowserbase) -> None: + async def test_union_response_different_types(self, respx_mock: MockRouter, async_client: AsyncStagehand) -> None: """Union of objects with the same field name using a different type""" class Model1(BaseModel): @@ -1383,7 +1373,7 @@ class Model2(BaseModel): @pytest.mark.respx(base_url=base_url) async def test_non_application_json_content_type_for_json_data( - self, respx_mock: MockRouter, async_client: AsyncBrowserbase + self, respx_mock: MockRouter, async_client: AsyncStagehand ) -> None: """ Response that sets Content-Type to something other than application/json but returns json data @@ -1405,7 +1395,7 @@ class Model(BaseModel): assert response.foo == 2 async def test_base_url_setter(self) -> None: - client = AsyncBrowserbase( + client = AsyncStagehand( base_url="https://example.com/from_init", api_key=api_key, _strict_response_validation=True ) assert client.base_url == "https://example.com/from_init/" @@ -1417,16 +1407,16 @@ async def test_base_url_setter(self) -> None: await client.close() async def test_base_url_env(self) -> None: - with update_env(BROWSERBASE_BASE_URL="http://localhost:5000/from/env"): - client = AsyncBrowserbase(api_key=api_key, _strict_response_validation=True) + with update_env(STAGEHAND_BASE_URL="http://localhost:5000/from/env"): + client = AsyncStagehand(api_key=api_key, _strict_response_validation=True) assert client.base_url == "http://localhost:5000/from/env/" # explicit environment arg requires explicitness - with update_env(BROWSERBASE_BASE_URL="http://localhost:5000/from/env"): + with update_env(STAGEHAND_BASE_URL="http://localhost:5000/from/env"): with pytest.raises(ValueError, match=r"you must pass base_url=None"): - AsyncBrowserbase(api_key=api_key, _strict_response_validation=True, environment="production") + AsyncStagehand(api_key=api_key, _strict_response_validation=True, environment="production") - client = AsyncBrowserbase( + client = AsyncStagehand( base_url=None, api_key=api_key, _strict_response_validation=True, environment="production" ) assert str(client.base_url).startswith("https://api.stagehand.browserbase.com/v1") @@ -1436,10 +1426,10 @@ async def test_base_url_env(self) -> None: @pytest.mark.parametrize( "client", [ - AsyncBrowserbase( + AsyncStagehand( base_url="http://localhost:5000/custom/path/", api_key=api_key, _strict_response_validation=True ), - AsyncBrowserbase( + AsyncStagehand( base_url="http://localhost:5000/custom/path/", api_key=api_key, _strict_response_validation=True, @@ -1448,7 +1438,7 @@ async def test_base_url_env(self) -> None: ], ids=["standard", "custom http client"], ) - async def test_base_url_trailing_slash(self, client: AsyncBrowserbase) -> None: + async def test_base_url_trailing_slash(self, client: AsyncStagehand) -> None: request = client._build_request( FinalRequestOptions( method="post", @@ -1462,10 +1452,10 @@ async def test_base_url_trailing_slash(self, client: AsyncBrowserbase) -> None: @pytest.mark.parametrize( "client", [ - AsyncBrowserbase( + AsyncStagehand( base_url="http://localhost:5000/custom/path/", api_key=api_key, _strict_response_validation=True ), - AsyncBrowserbase( + AsyncStagehand( base_url="http://localhost:5000/custom/path/", api_key=api_key, _strict_response_validation=True, @@ -1474,7 +1464,7 @@ async def test_base_url_trailing_slash(self, client: AsyncBrowserbase) -> None: ], ids=["standard", "custom http client"], ) - async def test_base_url_no_trailing_slash(self, client: AsyncBrowserbase) -> None: + async def test_base_url_no_trailing_slash(self, client: AsyncStagehand) -> None: request = client._build_request( FinalRequestOptions( method="post", @@ -1488,10 +1478,10 @@ async def test_base_url_no_trailing_slash(self, client: AsyncBrowserbase) -> Non @pytest.mark.parametrize( "client", [ - AsyncBrowserbase( + AsyncStagehand( base_url="http://localhost:5000/custom/path/", api_key=api_key, _strict_response_validation=True ), - AsyncBrowserbase( + AsyncStagehand( base_url="http://localhost:5000/custom/path/", api_key=api_key, _strict_response_validation=True, @@ -1500,7 +1490,7 @@ async def test_base_url_no_trailing_slash(self, client: AsyncBrowserbase) -> Non ], ids=["standard", "custom http client"], ) - async def test_absolute_request_url(self, client: AsyncBrowserbase) -> None: + async def test_absolute_request_url(self, client: AsyncStagehand) -> None: request = client._build_request( FinalRequestOptions( method="post", @@ -1512,7 +1502,7 @@ async def test_absolute_request_url(self, client: AsyncBrowserbase) -> None: await client.close() async def test_copied_client_does_not_close_http(self) -> None: - test_client = AsyncBrowserbase(base_url=base_url, api_key=api_key, _strict_response_validation=True) + test_client = AsyncStagehand(base_url=base_url, api_key=api_key, _strict_response_validation=True) assert not test_client.is_closed() copied = test_client.copy() @@ -1524,7 +1514,7 @@ async def test_copied_client_does_not_close_http(self) -> None: assert not test_client.is_closed() async def test_client_context_manager(self) -> None: - test_client = AsyncBrowserbase(base_url=base_url, api_key=api_key, _strict_response_validation=True) + test_client = AsyncStagehand(base_url=base_url, api_key=api_key, _strict_response_validation=True) async with test_client as c2: assert c2 is test_client assert not c2.is_closed() @@ -1532,9 +1522,7 @@ async def test_client_context_manager(self) -> None: assert test_client.is_closed() @pytest.mark.respx(base_url=base_url) - async def test_client_response_validation_error( - self, respx_mock: MockRouter, async_client: AsyncBrowserbase - ) -> None: + async def test_client_response_validation_error(self, respx_mock: MockRouter, async_client: AsyncStagehand) -> None: class Model(BaseModel): foo: str @@ -1547,7 +1535,7 @@ class Model(BaseModel): async def test_client_max_retries_validation(self) -> None: with pytest.raises(TypeError, match=r"max_retries cannot be None"): - AsyncBrowserbase( + AsyncStagehand( base_url=base_url, api_key=api_key, _strict_response_validation=True, max_retries=cast(Any, None) ) @@ -1558,12 +1546,12 @@ class Model(BaseModel): respx_mock.get("/foo").mock(return_value=httpx.Response(200, text="my-custom-format")) - strict_client = AsyncBrowserbase(base_url=base_url, api_key=api_key, _strict_response_validation=True) + strict_client = AsyncStagehand(base_url=base_url, api_key=api_key, _strict_response_validation=True) with pytest.raises(APIResponseValidationError): await strict_client.get("/foo", cast_to=Model) - non_strict_client = AsyncBrowserbase(base_url=base_url, api_key=api_key, _strict_response_validation=False) + non_strict_client = AsyncStagehand(base_url=base_url, api_key=api_key, _strict_response_validation=False) response = await non_strict_client.get("/foo", cast_to=Model) assert isinstance(response, str) # type: ignore[unreachable] @@ -1594,7 +1582,7 @@ class Model(BaseModel): ) @mock.patch("time.time", mock.MagicMock(return_value=1696004797)) async def test_parse_retry_after_header( - self, remaining_retries: int, retry_after: str, timeout: float, async_client: AsyncBrowserbase + self, remaining_retries: int, retry_after: str, timeout: float, async_client: AsyncStagehand ) -> None: headers = httpx.Headers({"retry-after": retry_after}) options = FinalRequestOptions(method="get", url="/foo", max_retries=3) @@ -1604,7 +1592,7 @@ async def test_parse_retry_after_header( @mock.patch("stagehand._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) @pytest.mark.respx(base_url=base_url) async def test_retrying_timeout_errors_doesnt_leak( - self, respx_mock: MockRouter, async_client: AsyncBrowserbase + self, respx_mock: MockRouter, async_client: AsyncStagehand ) -> None: respx_mock.post("/sessions/start").mock(side_effect=httpx.TimeoutException("Test timeout error")) @@ -1616,7 +1604,7 @@ async def test_retrying_timeout_errors_doesnt_leak( @mock.patch("stagehand._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) @pytest.mark.respx(base_url=base_url) async def test_retrying_status_errors_doesnt_leak( - self, respx_mock: MockRouter, async_client: AsyncBrowserbase + self, respx_mock: MockRouter, async_client: AsyncStagehand ) -> None: respx_mock.post("/sessions/start").mock(return_value=httpx.Response(500)) @@ -1630,7 +1618,7 @@ async def test_retrying_status_errors_doesnt_leak( @pytest.mark.parametrize("failure_mode", ["status", "exception"]) async def test_retries_taken( self, - async_client: AsyncBrowserbase, + async_client: AsyncStagehand, failures_before_success: int, failure_mode: Literal["status", "exception"], respx_mock: MockRouter, @@ -1659,7 +1647,7 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: @mock.patch("stagehand._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) @pytest.mark.respx(base_url=base_url) async def test_omit_retry_count_header( - self, async_client: AsyncBrowserbase, failures_before_success: int, respx_mock: MockRouter + self, async_client: AsyncStagehand, failures_before_success: int, respx_mock: MockRouter ) -> None: client = async_client.with_options(max_retries=4) @@ -1684,7 +1672,7 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: @mock.patch("stagehand._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) @pytest.mark.respx(base_url=base_url) async def test_overwrite_retry_count_header( - self, async_client: AsyncBrowserbase, failures_before_success: int, respx_mock: MockRouter + self, async_client: AsyncStagehand, failures_before_success: int, respx_mock: MockRouter ) -> None: client = async_client.with_options(max_retries=4) @@ -1732,7 +1720,7 @@ async def test_default_client_creation(self) -> None: ) @pytest.mark.respx(base_url=base_url) - async def test_follow_redirects(self, respx_mock: MockRouter, async_client: AsyncBrowserbase) -> None: + async def test_follow_redirects(self, respx_mock: MockRouter, async_client: AsyncStagehand) -> None: # Test that the default follow_redirects=True allows following redirects respx_mock.post("/redirect").mock( return_value=httpx.Response(302, headers={"Location": f"{base_url}/redirected"}) @@ -1744,7 +1732,7 @@ async def test_follow_redirects(self, respx_mock: MockRouter, async_client: Asyn assert response.json() == {"status": "ok"} @pytest.mark.respx(base_url=base_url) - async def test_follow_redirects_disabled(self, respx_mock: MockRouter, async_client: AsyncBrowserbase) -> None: + async def test_follow_redirects_disabled(self, respx_mock: MockRouter, async_client: AsyncStagehand) -> None: # Test that follow_redirects=False prevents following redirects respx_mock.post("/redirect").mock( return_value=httpx.Response(302, headers={"Location": f"{base_url}/redirected"}) diff --git a/tests/test_response.py b/tests/test_response.py index 5caa13ec..8da886ce 100644 --- a/tests/test_response.py +++ b/tests/test_response.py @@ -6,7 +6,7 @@ import pytest import pydantic -from stagehand import BaseModel, Browserbase, AsyncBrowserbase +from stagehand import BaseModel, Stagehand, AsyncStagehand from stagehand._response import ( APIResponse, BaseAPIResponse, @@ -56,7 +56,7 @@ def test_extract_response_type_binary_response() -> None: class PydanticModel(pydantic.BaseModel): ... -def test_response_parse_mismatched_basemodel(client: Browserbase) -> None: +def test_response_parse_mismatched_basemodel(client: Stagehand) -> None: response = APIResponse( raw=httpx.Response(200, content=b"foo"), client=client, @@ -74,7 +74,7 @@ def test_response_parse_mismatched_basemodel(client: Browserbase) -> None: @pytest.mark.asyncio -async def test_async_response_parse_mismatched_basemodel(async_client: AsyncBrowserbase) -> None: +async def test_async_response_parse_mismatched_basemodel(async_client: AsyncStagehand) -> None: response = AsyncAPIResponse( raw=httpx.Response(200, content=b"foo"), client=async_client, @@ -91,7 +91,7 @@ async def test_async_response_parse_mismatched_basemodel(async_client: AsyncBrow await response.parse(to=PydanticModel) -def test_response_parse_custom_stream(client: Browserbase) -> None: +def test_response_parse_custom_stream(client: Stagehand) -> None: response = APIResponse( raw=httpx.Response(200, content=b"foo"), client=client, @@ -106,7 +106,7 @@ def test_response_parse_custom_stream(client: Browserbase) -> None: @pytest.mark.asyncio -async def test_async_response_parse_custom_stream(async_client: AsyncBrowserbase) -> None: +async def test_async_response_parse_custom_stream(async_client: AsyncStagehand) -> None: response = AsyncAPIResponse( raw=httpx.Response(200, content=b"foo"), client=async_client, @@ -125,7 +125,7 @@ class CustomModel(BaseModel): bar: int -def test_response_parse_custom_model(client: Browserbase) -> None: +def test_response_parse_custom_model(client: Stagehand) -> None: response = APIResponse( raw=httpx.Response(200, content=json.dumps({"foo": "hello!", "bar": 2})), client=client, @@ -141,7 +141,7 @@ def test_response_parse_custom_model(client: Browserbase) -> None: @pytest.mark.asyncio -async def test_async_response_parse_custom_model(async_client: AsyncBrowserbase) -> None: +async def test_async_response_parse_custom_model(async_client: AsyncStagehand) -> None: response = AsyncAPIResponse( raw=httpx.Response(200, content=json.dumps({"foo": "hello!", "bar": 2})), client=async_client, @@ -156,7 +156,7 @@ async def test_async_response_parse_custom_model(async_client: AsyncBrowserbase) assert obj.bar == 2 -def test_response_parse_annotated_type(client: Browserbase) -> None: +def test_response_parse_annotated_type(client: Stagehand) -> None: response = APIResponse( raw=httpx.Response(200, content=json.dumps({"foo": "hello!", "bar": 2})), client=client, @@ -173,7 +173,7 @@ def test_response_parse_annotated_type(client: Browserbase) -> None: assert obj.bar == 2 -async def test_async_response_parse_annotated_type(async_client: AsyncBrowserbase) -> None: +async def test_async_response_parse_annotated_type(async_client: AsyncStagehand) -> None: response = AsyncAPIResponse( raw=httpx.Response(200, content=json.dumps({"foo": "hello!", "bar": 2})), client=async_client, @@ -201,7 +201,7 @@ async def test_async_response_parse_annotated_type(async_client: AsyncBrowserbas ("FalSe", False), ], ) -def test_response_parse_bool(client: Browserbase, content: str, expected: bool) -> None: +def test_response_parse_bool(client: Stagehand, content: str, expected: bool) -> None: response = APIResponse( raw=httpx.Response(200, content=content), client=client, @@ -226,7 +226,7 @@ def test_response_parse_bool(client: Browserbase, content: str, expected: bool) ("FalSe", False), ], ) -async def test_async_response_parse_bool(client: AsyncBrowserbase, content: str, expected: bool) -> None: +async def test_async_response_parse_bool(client: AsyncStagehand, content: str, expected: bool) -> None: response = AsyncAPIResponse( raw=httpx.Response(200, content=content), client=client, @@ -245,7 +245,7 @@ class OtherModel(BaseModel): @pytest.mark.parametrize("client", [False], indirect=True) # loose validation -def test_response_parse_expect_model_union_non_json_content(client: Browserbase) -> None: +def test_response_parse_expect_model_union_non_json_content(client: Stagehand) -> None: response = APIResponse( raw=httpx.Response(200, content=b"foo", headers={"Content-Type": "application/text"}), client=client, @@ -262,7 +262,7 @@ def test_response_parse_expect_model_union_non_json_content(client: Browserbase) @pytest.mark.asyncio @pytest.mark.parametrize("async_client", [False], indirect=True) # loose validation -async def test_async_response_parse_expect_model_union_non_json_content(async_client: AsyncBrowserbase) -> None: +async def test_async_response_parse_expect_model_union_non_json_content(async_client: AsyncStagehand) -> None: response = AsyncAPIResponse( raw=httpx.Response(200, content=b"foo", headers={"Content-Type": "application/text"}), client=async_client, diff --git a/tests/test_streaming.py b/tests/test_streaming.py index 5379ff9d..01423cbb 100644 --- a/tests/test_streaming.py +++ b/tests/test_streaming.py @@ -5,13 +5,13 @@ import httpx import pytest -from stagehand import Browserbase, AsyncBrowserbase +from stagehand import Stagehand, AsyncStagehand from stagehand._streaming import Stream, AsyncStream, ServerSentEvent @pytest.mark.asyncio @pytest.mark.parametrize("sync", [True, False], ids=["sync", "async"]) -async def test_basic(sync: bool, client: Browserbase, async_client: AsyncBrowserbase) -> None: +async def test_basic(sync: bool, client: Stagehand, async_client: AsyncStagehand) -> None: def body() -> Iterator[bytes]: yield b"event: completion\n" yield b'data: {"foo":true}\n' @@ -28,7 +28,7 @@ def body() -> Iterator[bytes]: @pytest.mark.asyncio @pytest.mark.parametrize("sync", [True, False], ids=["sync", "async"]) -async def test_data_missing_event(sync: bool, client: Browserbase, async_client: AsyncBrowserbase) -> None: +async def test_data_missing_event(sync: bool, client: Stagehand, async_client: AsyncStagehand) -> None: def body() -> Iterator[bytes]: yield b'data: {"foo":true}\n' yield b"\n" @@ -44,7 +44,7 @@ def body() -> Iterator[bytes]: @pytest.mark.asyncio @pytest.mark.parametrize("sync", [True, False], ids=["sync", "async"]) -async def test_event_missing_data(sync: bool, client: Browserbase, async_client: AsyncBrowserbase) -> None: +async def test_event_missing_data(sync: bool, client: Stagehand, async_client: AsyncStagehand) -> None: def body() -> Iterator[bytes]: yield b"event: ping\n" yield b"\n" @@ -60,7 +60,7 @@ def body() -> Iterator[bytes]: @pytest.mark.asyncio @pytest.mark.parametrize("sync", [True, False], ids=["sync", "async"]) -async def test_multiple_events(sync: bool, client: Browserbase, async_client: AsyncBrowserbase) -> None: +async def test_multiple_events(sync: bool, client: Stagehand, async_client: AsyncStagehand) -> None: def body() -> Iterator[bytes]: yield b"event: ping\n" yield b"\n" @@ -82,7 +82,7 @@ def body() -> Iterator[bytes]: @pytest.mark.asyncio @pytest.mark.parametrize("sync", [True, False], ids=["sync", "async"]) -async def test_multiple_events_with_data(sync: bool, client: Browserbase, async_client: AsyncBrowserbase) -> None: +async def test_multiple_events_with_data(sync: bool, client: Stagehand, async_client: AsyncStagehand) -> None: def body() -> Iterator[bytes]: yield b"event: ping\n" yield b'data: {"foo":true}\n' @@ -106,9 +106,7 @@ def body() -> Iterator[bytes]: @pytest.mark.asyncio @pytest.mark.parametrize("sync", [True, False], ids=["sync", "async"]) -async def test_multiple_data_lines_with_empty_line( - sync: bool, client: Browserbase, async_client: AsyncBrowserbase -) -> None: +async def test_multiple_data_lines_with_empty_line(sync: bool, client: Stagehand, async_client: AsyncStagehand) -> None: def body() -> Iterator[bytes]: yield b"event: ping\n" yield b"data: {\n" @@ -130,9 +128,7 @@ def body() -> Iterator[bytes]: @pytest.mark.asyncio @pytest.mark.parametrize("sync", [True, False], ids=["sync", "async"]) -async def test_data_json_escaped_double_new_line( - sync: bool, client: Browserbase, async_client: AsyncBrowserbase -) -> None: +async def test_data_json_escaped_double_new_line(sync: bool, client: Stagehand, async_client: AsyncStagehand) -> None: def body() -> Iterator[bytes]: yield b"event: ping\n" yield b'data: {"foo": "my long\\n\\ncontent"}' @@ -149,7 +145,7 @@ def body() -> Iterator[bytes]: @pytest.mark.asyncio @pytest.mark.parametrize("sync", [True, False], ids=["sync", "async"]) -async def test_multiple_data_lines(sync: bool, client: Browserbase, async_client: AsyncBrowserbase) -> None: +async def test_multiple_data_lines(sync: bool, client: Stagehand, async_client: AsyncStagehand) -> None: def body() -> Iterator[bytes]: yield b"event: ping\n" yield b"data: {\n" @@ -169,8 +165,8 @@ def body() -> Iterator[bytes]: @pytest.mark.parametrize("sync", [True, False], ids=["sync", "async"]) async def test_special_new_line_character( sync: bool, - client: Browserbase, - async_client: AsyncBrowserbase, + client: Stagehand, + async_client: AsyncStagehand, ) -> None: def body() -> Iterator[bytes]: yield b'data: {"content":" culpa"}\n' @@ -200,8 +196,8 @@ def body() -> Iterator[bytes]: @pytest.mark.parametrize("sync", [True, False], ids=["sync", "async"]) async def test_multi_byte_character_multiple_chunks( sync: bool, - client: Browserbase, - async_client: AsyncBrowserbase, + client: Stagehand, + async_client: AsyncStagehand, ) -> None: def body() -> Iterator[bytes]: yield b'data: {"content":"' @@ -241,8 +237,8 @@ def make_event_iterator( content: Iterator[bytes], *, sync: bool, - client: Browserbase, - async_client: AsyncBrowserbase, + client: Stagehand, + async_client: AsyncStagehand, ) -> Iterator[ServerSentEvent] | AsyncIterator[ServerSentEvent]: if sync: return Stream(cast_to=object, client=client, response=httpx.Response(200, content=content))._iter_events() From 6137b22411a498b2264aeeb560aeb0b3b8663660 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 16 Dec 2025 19:21:34 +0000 Subject: [PATCH 16/88] codegen metadata --- .stats.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.stats.yml b/.stats.yml index 8741890e..c1286482 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 7 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fstagehand-0c12f985340be2a9287e8e01ff8733f7f2d02e019149d1ae95f1a8f8798c6690.yml -openapi_spec_hash: efb79934e1dc63763dd4e8493b825273 -config_hash: 1de7cb9bd4dc46fe3e20b637bc534908 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fstagehand-cacb08bfd03b2d325b80318fc3f12bc7da16f229c21e9bd86050fff4ef7c67f7.yml +openapi_spec_hash: 21dc2123dc758a738591bf914410041a +config_hash: bb7561632c1f66c2b9efca06f438f904 From db85d60248ea84d207f024c2021ddc4b059303b8 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 16 Dec 2025 19:28:17 +0000 Subject: [PATCH 17/88] codegen metadata --- .stats.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index c1286482..a06fa532 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 7 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fstagehand-cacb08bfd03b2d325b80318fc3f12bc7da16f229c21e9bd86050fff4ef7c67f7.yml -openapi_spec_hash: 21dc2123dc758a738591bf914410041a +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fstagehand-09064f4021f94fb1b1bd86ce496d998318276b61bbc24de4728ecdb5763847ef.yml +openapi_spec_hash: 911d0631010b372890f98545a038cfb5 config_hash: bb7561632c1f66c2b9efca06f438f904 From bb82007170db17aad114df3831a7b1133220032c Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 16 Dec 2025 19:39:14 +0000 Subject: [PATCH 18/88] feat(api): manual updates --- .stats.yml | 6 +-- README.md | 45 ++++++++++++--------- src/stagehand/resources/sessions.py | 44 +++++++------------- src/stagehand/types/session_start_params.py | 26 ++++-------- tests/api_resources/test_sessions.py | 30 +++++++------- tests/test_client.py | 42 ++++++++++++++----- 6 files changed, 99 insertions(+), 94 deletions(-) diff --git a/.stats.yml b/.stats.yml index a06fa532..4664845d 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 7 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fstagehand-09064f4021f94fb1b1bd86ce496d998318276b61bbc24de4728ecdb5763847ef.yml -openapi_spec_hash: 911d0631010b372890f98545a038cfb5 -config_hash: bb7561632c1f66c2b9efca06f438f904 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fstagehand-6a22863a7da4fa45f904657a4cb8fc1a28e236925f03dc94fca25fd8271ca6db.yml +openapi_spec_hash: d5c6108942ad79f39ea4ff1bee9b7996 +config_hash: 2f1ec44e7e07906e07bdc6e075763da9 diff --git a/README.md b/README.md index 3a041fa0..cd5d7816 100644 --- a/README.md +++ b/README.md @@ -37,10 +37,11 @@ client = Stagehand( environment="dev", ) -response = client.sessions.start( - env="LOCAL", +response = client.sessions.act( + session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + input="click the first link on the page", ) -print(response.available) +print(response.actions) ``` While you can provide an `api_key` keyword argument, @@ -65,10 +66,11 @@ client = AsyncStagehand( async def main() -> None: - response = await client.sessions.start( - env="LOCAL", + response = await client.sessions.act( + session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + input="click the first link on the page", ) - print(response.available) + print(response.actions) asyncio.run(main()) @@ -101,10 +103,11 @@ async def main() -> None: api_key=os.environ.get("STAGEHAND_API_KEY"), # This is the default and can be omitted http_client=DefaultAioHttpClient(), ) as client: - response = await client.sessions.start( - env="LOCAL", + response = await client.sessions.act( + session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + input="click the first link on the page", ) - print(response.available) + print(response.actions) asyncio.run(main()) @@ -128,11 +131,12 @@ from stagehand import Stagehand client = Stagehand() -response = client.sessions.start( - env="LOCAL", - local_browser_launch_options={}, +response = client.sessions.act( + session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + input="click the sign in button", + options={}, ) -print(response.local_browser_launch_options) +print(response.options) ``` ## Handling errors @@ -152,7 +156,8 @@ client = Stagehand() try: client.sessions.start( - env="LOCAL", + browserbase_api_key="BROWSERBASE_API_KEY", + browserbase_project_id="BROWSERBASE_PROJECT_ID", ) except stagehand.APIConnectionError as e: print("The server could not be reached") @@ -197,7 +202,8 @@ client = Stagehand( # Or, configure per-request: client.with_options(max_retries=5).sessions.start( - env="LOCAL", + browserbase_api_key="BROWSERBASE_API_KEY", + browserbase_project_id="BROWSERBASE_PROJECT_ID", ) ``` @@ -222,7 +228,8 @@ client = Stagehand( # Override per-request: client.with_options(timeout=5.0).sessions.start( - env="LOCAL", + browserbase_api_key="BROWSERBASE_API_KEY", + browserbase_project_id="BROWSERBASE_PROJECT_ID", ) ``` @@ -265,7 +272,8 @@ from stagehand import Stagehand client = Stagehand() response = client.sessions.with_raw_response.start( - env="LOCAL", + browserbase_api_key="BROWSERBASE_API_KEY", + browserbase_project_id="BROWSERBASE_PROJECT_ID", ) print(response.headers.get('X-My-Header')) @@ -285,7 +293,8 @@ To stream the response body, use `.with_streaming_response` instead, which requi ```python with client.sessions.with_streaming_response.start( - env="LOCAL", + browserbase_api_key="BROWSERBASE_API_KEY", + browserbase_project_id="BROWSERBASE_PROJECT_ID", ) as response: print(response.headers.get("X-My-Header")) diff --git a/src/stagehand/resources/sessions.py b/src/stagehand/resources/sessions.py index b229fd92..6b76d2f8 100644 --- a/src/stagehand/resources/sessions.py +++ b/src/stagehand/resources/sessions.py @@ -375,12 +375,10 @@ def observe( def start( self, *, - env: Literal["LOCAL", "BROWSERBASE"], - api_key: str | Omit = omit, + browserbase_api_key: str, + browserbase_project_id: str, dom_settle_timeout: int | Omit = omit, - local_browser_launch_options: session_start_params.LocalBrowserLaunchOptions | Omit = omit, model: str | Omit = omit, - project_id: str | Omit = omit, self_heal: bool | Omit = omit, system_prompt: str | Omit = omit, verbose: int | Omit = omit, @@ -397,17 +395,13 @@ def start( ID that must be used for all subsequent requests. Args: - env: Environment to run the browser in + browserbase_api_key: API key for Browserbase Cloud - api_key: API key for Browserbase (required when env=BROWSERBASE) + browserbase_project_id: Project ID for Browserbase dom_settle_timeout: Timeout in ms to wait for DOM to settle - local_browser_launch_options: Options for local browser launch - - model: AI model to use for actions - - project_id: Project ID for Browserbase (required when env=BROWSERBASE) + model: AI model to use for actions (must be prefixed with provider/) self_heal: Enable self-healing for failed actions @@ -427,12 +421,10 @@ def start( "/sessions/start", body=maybe_transform( { - "env": env, - "api_key": api_key, + "browserbase_api_key": browserbase_api_key, + "browserbase_project_id": browserbase_project_id, "dom_settle_timeout": dom_settle_timeout, - "local_browser_launch_options": local_browser_launch_options, "model": model, - "project_id": project_id, "self_heal": self_heal, "system_prompt": system_prompt, "verbose": verbose, @@ -784,12 +776,10 @@ async def observe( async def start( self, *, - env: Literal["LOCAL", "BROWSERBASE"], - api_key: str | Omit = omit, + browserbase_api_key: str, + browserbase_project_id: str, dom_settle_timeout: int | Omit = omit, - local_browser_launch_options: session_start_params.LocalBrowserLaunchOptions | Omit = omit, model: str | Omit = omit, - project_id: str | Omit = omit, self_heal: bool | Omit = omit, system_prompt: str | Omit = omit, verbose: int | Omit = omit, @@ -806,17 +796,13 @@ async def start( ID that must be used for all subsequent requests. Args: - env: Environment to run the browser in + browserbase_api_key: API key for Browserbase Cloud - api_key: API key for Browserbase (required when env=BROWSERBASE) + browserbase_project_id: Project ID for Browserbase dom_settle_timeout: Timeout in ms to wait for DOM to settle - local_browser_launch_options: Options for local browser launch - - model: AI model to use for actions - - project_id: Project ID for Browserbase (required when env=BROWSERBASE) + model: AI model to use for actions (must be prefixed with provider/) self_heal: Enable self-healing for failed actions @@ -836,12 +822,10 @@ async def start( "/sessions/start", body=await async_maybe_transform( { - "env": env, - "api_key": api_key, + "browserbase_api_key": browserbase_api_key, + "browserbase_project_id": browserbase_project_id, "dom_settle_timeout": dom_settle_timeout, - "local_browser_launch_options": local_browser_launch_options, "model": model, - "project_id": project_id, "self_heal": self_heal, "system_prompt": system_prompt, "verbose": verbose, diff --git a/src/stagehand/types/session_start_params.py b/src/stagehand/types/session_start_params.py index fc7573c1..09320030 100644 --- a/src/stagehand/types/session_start_params.py +++ b/src/stagehand/types/session_start_params.py @@ -2,31 +2,25 @@ from __future__ import annotations -from typing_extensions import Literal, Required, Annotated, TypedDict +from typing_extensions import Required, Annotated, TypedDict from .._utils import PropertyInfo -__all__ = ["SessionStartParams", "LocalBrowserLaunchOptions"] +__all__ = ["SessionStartParams"] class SessionStartParams(TypedDict, total=False): - env: Required[Literal["LOCAL", "BROWSERBASE"]] - """Environment to run the browser in""" + browserbase_api_key: Required[Annotated[str, PropertyInfo(alias="BROWSERBASE_API_KEY")]] + """API key for Browserbase Cloud""" - api_key: Annotated[str, PropertyInfo(alias="apiKey")] - """API key for Browserbase (required when env=BROWSERBASE)""" + browserbase_project_id: Required[Annotated[str, PropertyInfo(alias="BROWSERBASE_PROJECT_ID")]] + """Project ID for Browserbase""" dom_settle_timeout: Annotated[int, PropertyInfo(alias="domSettleTimeout")] """Timeout in ms to wait for DOM to settle""" - local_browser_launch_options: Annotated[LocalBrowserLaunchOptions, PropertyInfo(alias="localBrowserLaunchOptions")] - """Options for local browser launch""" - model: str - """AI model to use for actions""" - - project_id: Annotated[str, PropertyInfo(alias="projectId")] - """Project ID for Browserbase (required when env=BROWSERBASE)""" + """AI model to use for actions (must be prefixed with provider/)""" self_heal: Annotated[bool, PropertyInfo(alias="selfHeal")] """Enable self-healing for failed actions""" @@ -36,9 +30,3 @@ class SessionStartParams(TypedDict, total=False): verbose: int """Logging verbosity level""" - - -class LocalBrowserLaunchOptions(TypedDict, total=False): - """Options for local browser launch""" - - headless: bool diff --git a/tests/api_resources/test_sessions.py b/tests/api_resources/test_sessions.py index 2c0829d5..f38e02d6 100644 --- a/tests/api_resources/test_sessions.py +++ b/tests/api_resources/test_sessions.py @@ -394,7 +394,8 @@ def test_path_params_observe(self, client: Stagehand) -> None: @parametrize def test_method_start(self, client: Stagehand) -> None: session = client.sessions.start( - env="LOCAL", + browserbase_api_key="BROWSERBASE_API_KEY", + browserbase_project_id="BROWSERBASE_PROJECT_ID", ) assert_matches_type(SessionStartResponse, session, path=["response"]) @@ -402,12 +403,10 @@ def test_method_start(self, client: Stagehand) -> None: @parametrize def test_method_start_with_all_params(self, client: Stagehand) -> None: session = client.sessions.start( - env="LOCAL", - api_key="apiKey", + browserbase_api_key="BROWSERBASE_API_KEY", + browserbase_project_id="BROWSERBASE_PROJECT_ID", dom_settle_timeout=0, - local_browser_launch_options={"headless": True}, model="openai/gpt-4o", - project_id="projectId", self_heal=True, system_prompt="systemPrompt", verbose=1, @@ -418,7 +417,8 @@ def test_method_start_with_all_params(self, client: Stagehand) -> None: @parametrize def test_raw_response_start(self, client: Stagehand) -> None: response = client.sessions.with_raw_response.start( - env="LOCAL", + browserbase_api_key="BROWSERBASE_API_KEY", + browserbase_project_id="BROWSERBASE_PROJECT_ID", ) assert response.is_closed is True @@ -430,7 +430,8 @@ def test_raw_response_start(self, client: Stagehand) -> None: @parametrize def test_streaming_response_start(self, client: Stagehand) -> None: with client.sessions.with_streaming_response.start( - env="LOCAL", + browserbase_api_key="BROWSERBASE_API_KEY", + browserbase_project_id="BROWSERBASE_PROJECT_ID", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -815,7 +816,8 @@ async def test_path_params_observe(self, async_client: AsyncStagehand) -> None: @parametrize async def test_method_start(self, async_client: AsyncStagehand) -> None: session = await async_client.sessions.start( - env="LOCAL", + browserbase_api_key="BROWSERBASE_API_KEY", + browserbase_project_id="BROWSERBASE_PROJECT_ID", ) assert_matches_type(SessionStartResponse, session, path=["response"]) @@ -823,12 +825,10 @@ async def test_method_start(self, async_client: AsyncStagehand) -> None: @parametrize async def test_method_start_with_all_params(self, async_client: AsyncStagehand) -> None: session = await async_client.sessions.start( - env="LOCAL", - api_key="apiKey", + browserbase_api_key="BROWSERBASE_API_KEY", + browserbase_project_id="BROWSERBASE_PROJECT_ID", dom_settle_timeout=0, - local_browser_launch_options={"headless": True}, model="openai/gpt-4o", - project_id="projectId", self_heal=True, system_prompt="systemPrompt", verbose=1, @@ -839,7 +839,8 @@ async def test_method_start_with_all_params(self, async_client: AsyncStagehand) @parametrize async def test_raw_response_start(self, async_client: AsyncStagehand) -> None: response = await async_client.sessions.with_raw_response.start( - env="LOCAL", + browserbase_api_key="BROWSERBASE_API_KEY", + browserbase_project_id="BROWSERBASE_PROJECT_ID", ) assert response.is_closed is True @@ -851,7 +852,8 @@ async def test_raw_response_start(self, async_client: AsyncStagehand) -> None: @parametrize async def test_streaming_response_start(self, async_client: AsyncStagehand) -> None: async with async_client.sessions.with_streaming_response.start( - env="LOCAL", + browserbase_api_key="BROWSERBASE_API_KEY", + browserbase_project_id="BROWSERBASE_PROJECT_ID", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" diff --git a/tests/test_client.py b/tests/test_client.py index fec791da..de8e05af 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -749,7 +749,9 @@ def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter, clien respx_mock.post("/sessions/start").mock(side_effect=httpx.TimeoutException("Test timeout error")) with pytest.raises(APITimeoutError): - client.sessions.with_streaming_response.start(env="LOCAL").__enter__() + client.sessions.with_streaming_response.start( + browserbase_api_key="BROWSERBASE_API_KEY", browserbase_project_id="BROWSERBASE_PROJECT_ID" + ).__enter__() assert _get_open_connections(client) == 0 @@ -759,7 +761,9 @@ def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter, client respx_mock.post("/sessions/start").mock(return_value=httpx.Response(500)) with pytest.raises(APIStatusError): - client.sessions.with_streaming_response.start(env="LOCAL").__enter__() + client.sessions.with_streaming_response.start( + browserbase_api_key="BROWSERBASE_API_KEY", browserbase_project_id="BROWSERBASE_PROJECT_ID" + ).__enter__() assert _get_open_connections(client) == 0 @pytest.mark.parametrize("failures_before_success", [0, 2, 4]) @@ -788,7 +792,9 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: respx_mock.post("/sessions/start").mock(side_effect=retry_handler) - response = client.sessions.with_raw_response.start(env="LOCAL") + response = client.sessions.with_raw_response.start( + browserbase_api_key="BROWSERBASE_API_KEY", browserbase_project_id="BROWSERBASE_PROJECT_ID" + ) assert response.retries_taken == failures_before_success assert int(response.http_request.headers.get("x-stainless-retry-count")) == failures_before_success @@ -813,7 +819,9 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: respx_mock.post("/sessions/start").mock(side_effect=retry_handler) response = client.sessions.with_raw_response.start( - env="LOCAL", extra_headers={"x-stainless-retry-count": Omit()} + browserbase_api_key="BROWSERBASE_API_KEY", + browserbase_project_id="BROWSERBASE_PROJECT_ID", + extra_headers={"x-stainless-retry-count": Omit()}, ) assert len(response.http_request.headers.get_list("x-stainless-retry-count")) == 0 @@ -837,7 +845,11 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: respx_mock.post("/sessions/start").mock(side_effect=retry_handler) - response = client.sessions.with_raw_response.start(env="LOCAL", extra_headers={"x-stainless-retry-count": "42"}) + response = client.sessions.with_raw_response.start( + browserbase_api_key="BROWSERBASE_API_KEY", + browserbase_project_id="BROWSERBASE_PROJECT_ID", + extra_headers={"x-stainless-retry-count": "42"}, + ) assert response.http_request.headers.get("x-stainless-retry-count") == "42" @@ -1597,7 +1609,9 @@ async def test_retrying_timeout_errors_doesnt_leak( respx_mock.post("/sessions/start").mock(side_effect=httpx.TimeoutException("Test timeout error")) with pytest.raises(APITimeoutError): - await async_client.sessions.with_streaming_response.start(env="LOCAL").__aenter__() + await async_client.sessions.with_streaming_response.start( + browserbase_api_key="BROWSERBASE_API_KEY", browserbase_project_id="BROWSERBASE_PROJECT_ID" + ).__aenter__() assert _get_open_connections(async_client) == 0 @@ -1609,7 +1623,9 @@ async def test_retrying_status_errors_doesnt_leak( respx_mock.post("/sessions/start").mock(return_value=httpx.Response(500)) with pytest.raises(APIStatusError): - await async_client.sessions.with_streaming_response.start(env="LOCAL").__aenter__() + await async_client.sessions.with_streaming_response.start( + browserbase_api_key="BROWSERBASE_API_KEY", browserbase_project_id="BROWSERBASE_PROJECT_ID" + ).__aenter__() assert _get_open_connections(async_client) == 0 @pytest.mark.parametrize("failures_before_success", [0, 2, 4]) @@ -1638,7 +1654,9 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: respx_mock.post("/sessions/start").mock(side_effect=retry_handler) - response = await client.sessions.with_raw_response.start(env="LOCAL") + response = await client.sessions.with_raw_response.start( + browserbase_api_key="BROWSERBASE_API_KEY", browserbase_project_id="BROWSERBASE_PROJECT_ID" + ) assert response.retries_taken == failures_before_success assert int(response.http_request.headers.get("x-stainless-retry-count")) == failures_before_success @@ -1663,7 +1681,9 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: respx_mock.post("/sessions/start").mock(side_effect=retry_handler) response = await client.sessions.with_raw_response.start( - env="LOCAL", extra_headers={"x-stainless-retry-count": Omit()} + browserbase_api_key="BROWSERBASE_API_KEY", + browserbase_project_id="BROWSERBASE_PROJECT_ID", + extra_headers={"x-stainless-retry-count": Omit()}, ) assert len(response.http_request.headers.get_list("x-stainless-retry-count")) == 0 @@ -1688,7 +1708,9 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: respx_mock.post("/sessions/start").mock(side_effect=retry_handler) response = await client.sessions.with_raw_response.start( - env="LOCAL", extra_headers={"x-stainless-retry-count": "42"} + browserbase_api_key="BROWSERBASE_API_KEY", + browserbase_project_id="BROWSERBASE_PROJECT_ID", + extra_headers={"x-stainless-retry-count": "42"}, ) assert response.http_request.headers.get("x-stainless-retry-count") == "42" From 01266ac8dbf44a830db19627a6550b9eaa406fe9 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 16 Dec 2025 19:40:12 +0000 Subject: [PATCH 19/88] codegen metadata --- .stats.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index 4664845d..54cc9782 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 7 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fstagehand-6a22863a7da4fa45f904657a4cb8fc1a28e236925f03dc94fca25fd8271ca6db.yml -openapi_spec_hash: d5c6108942ad79f39ea4ff1bee9b7996 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fstagehand-3607b588cab78536eb7de9f6acffe8ddda1d34aebe5910c2147421aa6c16bf22.yml +openapi_spec_hash: fb507e8d38b4978a5717fbb144197868 config_hash: 2f1ec44e7e07906e07bdc6e075763da9 From 90397ea2c4c803dbec9329bc45b9ab78a9594bcf Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 16 Dec 2025 20:08:22 +0000 Subject: [PATCH 20/88] feat(api): manual updates --- .stats.yml | 2 +- README.md | 34 +++- src/stagehand/__init__.py | 2 - src/stagehand/_client.py | 213 +++++++++++--------- tests/conftest.py | 19 +- tests/test_client.py | 414 ++++++++++++++++++++++++++++++-------- 6 files changed, 488 insertions(+), 196 deletions(-) diff --git a/.stats.yml b/.stats.yml index 54cc9782..1e160655 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 7 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fstagehand-3607b588cab78536eb7de9f6acffe8ddda1d34aebe5910c2147421aa6c16bf22.yml openapi_spec_hash: fb507e8d38b4978a5717fbb144197868 -config_hash: 2f1ec44e7e07906e07bdc6e075763da9 +config_hash: fc6606301b5142487a69d296f154b265 diff --git a/README.md b/README.md index cd5d7816..b87f9fc1 100644 --- a/README.md +++ b/README.md @@ -32,9 +32,13 @@ import os from stagehand import Stagehand client = Stagehand( - api_key=os.environ.get("STAGEHAND_API_KEY"), # This is the default and can be omitted - # or 'production' | 'local'; defaults to "production". - environment="dev", + browserbase_api_key=os.environ.get( + "BROWSERBASE_API_KEY" + ), # This is the default and can be omitted + browserbase_project_id=os.environ.get( + "BROWSERBASE_PROJECT_ID" + ), # This is the default and can be omitted + model_api_key=os.environ.get("MODEL_API_KEY"), # This is the default and can be omitted ) response = client.sessions.act( @@ -44,10 +48,10 @@ response = client.sessions.act( print(response.actions) ``` -While you can provide an `api_key` keyword argument, +While you can provide a `browserbase_api_key` keyword argument, we recommend using [python-dotenv](https://pypi.org/project/python-dotenv/) -to add `STAGEHAND_API_KEY="My API Key"` to your `.env` file -so that your API Key is not stored in source control. +to add `BROWSERBASE_API_KEY="My Browserbase API Key"` to your `.env` file +so that your Browserbase API Key is not stored in source control. ## Async usage @@ -59,9 +63,13 @@ import asyncio from stagehand import AsyncStagehand client = AsyncStagehand( - api_key=os.environ.get("STAGEHAND_API_KEY"), # This is the default and can be omitted - # or 'production' | 'local'; defaults to "production". - environment="dev", + browserbase_api_key=os.environ.get( + "BROWSERBASE_API_KEY" + ), # This is the default and can be omitted + browserbase_project_id=os.environ.get( + "BROWSERBASE_PROJECT_ID" + ), # This is the default and can be omitted + model_api_key=os.environ.get("MODEL_API_KEY"), # This is the default and can be omitted ) @@ -100,7 +108,13 @@ from stagehand import AsyncStagehand async def main() -> None: async with AsyncStagehand( - api_key=os.environ.get("STAGEHAND_API_KEY"), # This is the default and can be omitted + browserbase_api_key=os.environ.get( + "BROWSERBASE_API_KEY" + ), # This is the default and can be omitted + browserbase_project_id=os.environ.get( + "BROWSERBASE_PROJECT_ID" + ), # This is the default and can be omitted + model_api_key=os.environ.get("MODEL_API_KEY"), # This is the default and can be omitted http_client=DefaultAioHttpClient(), ) as client: response = await client.sessions.act( diff --git a/src/stagehand/__init__.py b/src/stagehand/__init__.py index 44c6dfcc..95c793d7 100644 --- a/src/stagehand/__init__.py +++ b/src/stagehand/__init__.py @@ -6,7 +6,6 @@ from ._types import NOT_GIVEN, Omit, NoneType, NotGiven, Transport, ProxiesTypes, omit, not_given from ._utils import file_from_path from ._client import ( - ENVIRONMENTS, Client, Stream, Timeout, @@ -74,7 +73,6 @@ "AsyncStream", "Stagehand", "AsyncStagehand", - "ENVIRONMENTS", "file_from_path", "BaseModel", "DEFAULT_TIMEOUT", diff --git a/src/stagehand/_client.py b/src/stagehand/_client.py index cf9608c6..6669ffdb 100644 --- a/src/stagehand/_client.py +++ b/src/stagehand/_client.py @@ -3,8 +3,8 @@ from __future__ import annotations import os -from typing import Any, Dict, Mapping, cast -from typing_extensions import Self, Literal, override +from typing import Any, Mapping +from typing_extensions import Self, override import httpx @@ -31,7 +31,6 @@ ) __all__ = [ - "ENVIRONMENTS", "Timeout", "Transport", "ProxiesTypes", @@ -42,12 +41,6 @@ "AsyncClient", ] -ENVIRONMENTS: Dict[str, str] = { - "production": "https://api.stagehand.browserbase.com/v1", - "dev": "https://api.stagehand.dev.browserbase.com/v1", - "local": "http://localhost:5000/v1", -} - class Stagehand(SyncAPIClient): sessions: sessions.SessionsResource @@ -55,16 +48,17 @@ class Stagehand(SyncAPIClient): with_streaming_response: StagehandWithStreamedResponse # client options - api_key: str - - _environment: Literal["production", "dev", "local"] | NotGiven + browserbase_api_key: str + browserbase_project_id: str + model_api_key: str | None def __init__( self, *, - api_key: str | None = None, - environment: Literal["production", "dev", "local"] | NotGiven = not_given, - base_url: str | httpx.URL | None | NotGiven = not_given, + browserbase_api_key: str | None = None, + browserbase_project_id: str | None = None, + model_api_key: str | None = None, + base_url: str | httpx.URL | None = None, timeout: float | Timeout | None | NotGiven = not_given, max_retries: int = DEFAULT_MAX_RETRIES, default_headers: Mapping[str, str] | None = None, @@ -85,41 +79,35 @@ def __init__( ) -> None: """Construct a new synchronous Stagehand client instance. - This automatically infers the `api_key` argument from the `STAGEHAND_API_KEY` environment variable if it is not provided. + This automatically infers the following arguments from their corresponding environment variables if they are not provided: + - `browserbase_api_key` from `BROWSERBASE_API_KEY` + - `browserbase_project_id` from `BROWSERBASE_PROJECT_ID` + - `model_api_key` from `MODEL_API_KEY` """ - if api_key is None: - api_key = os.environ.get("STAGEHAND_API_KEY") - if api_key is None: + if browserbase_api_key is None: + browserbase_api_key = os.environ.get("BROWSERBASE_API_KEY") + if browserbase_api_key is None: raise StagehandError( - "The api_key client option must be set either by passing api_key to the client or by setting the STAGEHAND_API_KEY environment variable" + "The browserbase_api_key client option must be set either by passing browserbase_api_key to the client or by setting the BROWSERBASE_API_KEY environment variable" ) - self.api_key = api_key - - self._environment = environment - - base_url_env = os.environ.get("STAGEHAND_BASE_URL") - if is_given(base_url) and base_url is not None: - # cast required because mypy doesn't understand the type narrowing - base_url = cast("str | httpx.URL", base_url) # pyright: ignore[reportUnnecessaryCast] - elif is_given(environment): - if base_url_env and base_url is not None: - raise ValueError( - "Ambiguous URL; The `STAGEHAND_BASE_URL` env var and the `environment` argument are given. If you want to use the environment, you must pass base_url=None", - ) - - try: - base_url = ENVIRONMENTS[environment] - except KeyError as exc: - raise ValueError(f"Unknown environment: {environment}") from exc - elif base_url_env is not None: - base_url = base_url_env - else: - self._environment = environment = "production" - - try: - base_url = ENVIRONMENTS[environment] - except KeyError as exc: - raise ValueError(f"Unknown environment: {environment}") from exc + self.browserbase_api_key = browserbase_api_key + + if browserbase_project_id is None: + browserbase_project_id = os.environ.get("BROWSERBASE_PROJECT_ID") + if browserbase_project_id is None: + raise StagehandError( + "The browserbase_project_id client option must be set either by passing browserbase_project_id to the client or by setting the BROWSERBASE_PROJECT_ID environment variable" + ) + self.browserbase_project_id = browserbase_project_id + + if model_api_key is None: + model_api_key = os.environ.get("MODEL_API_KEY") + self.model_api_key = model_api_key + + if base_url is None: + base_url = os.environ.get("STAGEHAND_BASE_URL") + if base_url is None: + base_url = f"https://api.stagehand.browserbase.com/v1" super().__init__( version=__version__, @@ -144,8 +132,24 @@ def qs(self) -> Querystring: @property @override def auth_headers(self) -> dict[str, str]: - api_key = self.api_key - return {"Authorization": f"Bearer {api_key}"} + return {**self._bb_api_key_auth, **self._bb_project_id_auth, **self._llm_model_api_key_auth} + + @property + def _bb_api_key_auth(self) -> dict[str, str]: + browserbase_api_key = self.browserbase_api_key + return {"x-bb-api-key": browserbase_api_key} + + @property + def _bb_project_id_auth(self) -> dict[str, str]: + browserbase_project_id = self.browserbase_project_id + return {"x-bb-project-id": browserbase_project_id} + + @property + def _llm_model_api_key_auth(self) -> dict[str, str]: + model_api_key = self.model_api_key + if model_api_key is None: + return {} + return {"x-model-api-key": model_api_key} @property @override @@ -159,8 +163,9 @@ def default_headers(self) -> dict[str, str | Omit]: def copy( self, *, - api_key: str | None = None, - environment: Literal["production", "dev", "local"] | None = None, + browserbase_api_key: str | None = None, + browserbase_project_id: str | None = None, + model_api_key: str | None = None, base_url: str | httpx.URL | None = None, timeout: float | Timeout | None | NotGiven = not_given, http_client: httpx.Client | None = None, @@ -194,9 +199,10 @@ def copy( http_client = http_client or self._client return self.__class__( - api_key=api_key or self.api_key, + browserbase_api_key=browserbase_api_key or self.browserbase_api_key, + browserbase_project_id=browserbase_project_id or self.browserbase_project_id, + model_api_key=model_api_key or self.model_api_key, base_url=base_url or self.base_url, - environment=environment or self._environment, timeout=self.timeout if isinstance(timeout, NotGiven) else timeout, http_client=http_client, max_retries=max_retries if is_given(max_retries) else self.max_retries, @@ -249,16 +255,17 @@ class AsyncStagehand(AsyncAPIClient): with_streaming_response: AsyncStagehandWithStreamedResponse # client options - api_key: str - - _environment: Literal["production", "dev", "local"] | NotGiven + browserbase_api_key: str + browserbase_project_id: str + model_api_key: str | None def __init__( self, *, - api_key: str | None = None, - environment: Literal["production", "dev", "local"] | NotGiven = not_given, - base_url: str | httpx.URL | None | NotGiven = not_given, + browserbase_api_key: str | None = None, + browserbase_project_id: str | None = None, + model_api_key: str | None = None, + base_url: str | httpx.URL | None = None, timeout: float | Timeout | None | NotGiven = not_given, max_retries: int = DEFAULT_MAX_RETRIES, default_headers: Mapping[str, str] | None = None, @@ -279,41 +286,35 @@ def __init__( ) -> None: """Construct a new async AsyncStagehand client instance. - This automatically infers the `api_key` argument from the `STAGEHAND_API_KEY` environment variable if it is not provided. + This automatically infers the following arguments from their corresponding environment variables if they are not provided: + - `browserbase_api_key` from `BROWSERBASE_API_KEY` + - `browserbase_project_id` from `BROWSERBASE_PROJECT_ID` + - `model_api_key` from `MODEL_API_KEY` """ - if api_key is None: - api_key = os.environ.get("STAGEHAND_API_KEY") - if api_key is None: + if browserbase_api_key is None: + browserbase_api_key = os.environ.get("BROWSERBASE_API_KEY") + if browserbase_api_key is None: raise StagehandError( - "The api_key client option must be set either by passing api_key to the client or by setting the STAGEHAND_API_KEY environment variable" + "The browserbase_api_key client option must be set either by passing browserbase_api_key to the client or by setting the BROWSERBASE_API_KEY environment variable" ) - self.api_key = api_key - - self._environment = environment - - base_url_env = os.environ.get("STAGEHAND_BASE_URL") - if is_given(base_url) and base_url is not None: - # cast required because mypy doesn't understand the type narrowing - base_url = cast("str | httpx.URL", base_url) # pyright: ignore[reportUnnecessaryCast] - elif is_given(environment): - if base_url_env and base_url is not None: - raise ValueError( - "Ambiguous URL; The `STAGEHAND_BASE_URL` env var and the `environment` argument are given. If you want to use the environment, you must pass base_url=None", - ) - - try: - base_url = ENVIRONMENTS[environment] - except KeyError as exc: - raise ValueError(f"Unknown environment: {environment}") from exc - elif base_url_env is not None: - base_url = base_url_env - else: - self._environment = environment = "production" - - try: - base_url = ENVIRONMENTS[environment] - except KeyError as exc: - raise ValueError(f"Unknown environment: {environment}") from exc + self.browserbase_api_key = browserbase_api_key + + if browserbase_project_id is None: + browserbase_project_id = os.environ.get("BROWSERBASE_PROJECT_ID") + if browserbase_project_id is None: + raise StagehandError( + "The browserbase_project_id client option must be set either by passing browserbase_project_id to the client or by setting the BROWSERBASE_PROJECT_ID environment variable" + ) + self.browserbase_project_id = browserbase_project_id + + if model_api_key is None: + model_api_key = os.environ.get("MODEL_API_KEY") + self.model_api_key = model_api_key + + if base_url is None: + base_url = os.environ.get("STAGEHAND_BASE_URL") + if base_url is None: + base_url = f"https://api.stagehand.browserbase.com/v1" super().__init__( version=__version__, @@ -338,8 +339,24 @@ def qs(self) -> Querystring: @property @override def auth_headers(self) -> dict[str, str]: - api_key = self.api_key - return {"Authorization": f"Bearer {api_key}"} + return {**self._bb_api_key_auth, **self._bb_project_id_auth, **self._llm_model_api_key_auth} + + @property + def _bb_api_key_auth(self) -> dict[str, str]: + browserbase_api_key = self.browserbase_api_key + return {"x-bb-api-key": browserbase_api_key} + + @property + def _bb_project_id_auth(self) -> dict[str, str]: + browserbase_project_id = self.browserbase_project_id + return {"x-bb-project-id": browserbase_project_id} + + @property + def _llm_model_api_key_auth(self) -> dict[str, str]: + model_api_key = self.model_api_key + if model_api_key is None: + return {} + return {"x-model-api-key": model_api_key} @property @override @@ -353,8 +370,9 @@ def default_headers(self) -> dict[str, str | Omit]: def copy( self, *, - api_key: str | None = None, - environment: Literal["production", "dev", "local"] | None = None, + browserbase_api_key: str | None = None, + browserbase_project_id: str | None = None, + model_api_key: str | None = None, base_url: str | httpx.URL | None = None, timeout: float | Timeout | None | NotGiven = not_given, http_client: httpx.AsyncClient | None = None, @@ -388,9 +406,10 @@ def copy( http_client = http_client or self._client return self.__class__( - api_key=api_key or self.api_key, + browserbase_api_key=browserbase_api_key or self.browserbase_api_key, + browserbase_project_id=browserbase_project_id or self.browserbase_project_id, + model_api_key=model_api_key or self.model_api_key, base_url=base_url or self.base_url, - environment=environment or self._environment, timeout=self.timeout if isinstance(timeout, NotGiven) else timeout, http_client=http_client, max_retries=max_retries if is_given(max_retries) else self.max_retries, diff --git a/tests/conftest.py b/tests/conftest.py index f57b7286..9a14e186 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -45,7 +45,9 @@ def pytest_collection_modifyitems(items: list[pytest.Function]) -> None: base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") -api_key = "My API Key" +browserbase_api_key = "My Browserbase API Key" +browserbase_project_id = "My Browserbase Project ID" +model_api_key = "My Model API Key" @pytest.fixture(scope="session") @@ -54,7 +56,13 @@ def client(request: FixtureRequest) -> Iterator[Stagehand]: if not isinstance(strict, bool): raise TypeError(f"Unexpected fixture parameter type {type(strict)}, expected {bool}") - with Stagehand(base_url=base_url, api_key=api_key, _strict_response_validation=strict) as client: + with Stagehand( + base_url=base_url, + browserbase_api_key=browserbase_api_key, + browserbase_project_id=browserbase_project_id, + model_api_key=model_api_key, + _strict_response_validation=strict, + ) as client: yield client @@ -79,6 +87,11 @@ async def async_client(request: FixtureRequest) -> AsyncIterator[AsyncStagehand] raise TypeError(f"Unexpected fixture parameter type {type(param)}, expected bool or dict") async with AsyncStagehand( - base_url=base_url, api_key=api_key, _strict_response_validation=strict, http_client=http_client + base_url=base_url, + browserbase_api_key=browserbase_api_key, + browserbase_project_id=browserbase_project_id, + model_api_key=model_api_key, + _strict_response_validation=strict, + http_client=http_client, ) as client: yield client diff --git a/tests/test_client.py b/tests/test_client.py index de8e05af..7f28654f 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -37,7 +37,9 @@ from .utils import update_env base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") -api_key = "My API Key" +browserbase_api_key = "My Browserbase API Key" +browserbase_project_id = "My Browserbase Project ID" +model_api_key = "My Model API Key" def _get_params(client: BaseClient[Any, Any]) -> dict[str, str]: @@ -83,9 +85,17 @@ def test_copy(self, client: Stagehand) -> None: copied = client.copy() assert id(copied) != id(client) - copied = client.copy(api_key="another My API Key") - assert copied.api_key == "another My API Key" - assert client.api_key == "My API Key" + copied = client.copy(browserbase_api_key="another My Browserbase API Key") + assert copied.browserbase_api_key == "another My Browserbase API Key" + assert client.browserbase_api_key == "My Browserbase API Key" + + copied = client.copy(browserbase_project_id="another My Browserbase Project ID") + assert copied.browserbase_project_id == "another My Browserbase Project ID" + assert client.browserbase_project_id == "My Browserbase Project ID" + + copied = client.copy(model_api_key="another My Model API Key") + assert copied.model_api_key == "another My Model API Key" + assert client.model_api_key == "My Model API Key" def test_copy_default_options(self, client: Stagehand) -> None: # options that have a default are overridden correctly @@ -105,7 +115,12 @@ def test_copy_default_options(self, client: Stagehand) -> None: def test_copy_default_headers(self) -> None: client = Stagehand( - base_url=base_url, api_key=api_key, _strict_response_validation=True, default_headers={"X-Foo": "bar"} + base_url=base_url, + browserbase_api_key=browserbase_api_key, + browserbase_project_id=browserbase_project_id, + model_api_key=model_api_key, + _strict_response_validation=True, + default_headers={"X-Foo": "bar"}, ) assert client.default_headers["X-Foo"] == "bar" @@ -140,7 +155,12 @@ def test_copy_default_headers(self) -> None: def test_copy_default_query(self) -> None: client = Stagehand( - base_url=base_url, api_key=api_key, _strict_response_validation=True, default_query={"foo": "bar"} + base_url=base_url, + browserbase_api_key=browserbase_api_key, + browserbase_project_id=browserbase_project_id, + model_api_key=model_api_key, + _strict_response_validation=True, + default_query={"foo": "bar"}, ) assert _get_params(client)["foo"] == "bar" @@ -266,7 +286,12 @@ def test_request_timeout(self, client: Stagehand) -> None: def test_client_timeout_option(self) -> None: client = Stagehand( - base_url=base_url, api_key=api_key, _strict_response_validation=True, timeout=httpx.Timeout(0) + base_url=base_url, + browserbase_api_key=browserbase_api_key, + browserbase_project_id=browserbase_project_id, + model_api_key=model_api_key, + _strict_response_validation=True, + timeout=httpx.Timeout(0), ) request = client._build_request(FinalRequestOptions(method="get", url="/foo")) @@ -279,7 +304,12 @@ def test_http_client_timeout_option(self) -> None: # custom timeout given to the httpx client should be used with httpx.Client(timeout=None) as http_client: client = Stagehand( - base_url=base_url, api_key=api_key, _strict_response_validation=True, http_client=http_client + base_url=base_url, + browserbase_api_key=browserbase_api_key, + browserbase_project_id=browserbase_project_id, + model_api_key=model_api_key, + _strict_response_validation=True, + http_client=http_client, ) request = client._build_request(FinalRequestOptions(method="get", url="/foo")) @@ -291,7 +321,12 @@ def test_http_client_timeout_option(self) -> None: # no timeout given to the httpx client should not use the httpx default with httpx.Client() as http_client: client = Stagehand( - base_url=base_url, api_key=api_key, _strict_response_validation=True, http_client=http_client + base_url=base_url, + browserbase_api_key=browserbase_api_key, + browserbase_project_id=browserbase_project_id, + model_api_key=model_api_key, + _strict_response_validation=True, + http_client=http_client, ) request = client._build_request(FinalRequestOptions(method="get", url="/foo")) @@ -303,7 +338,12 @@ def test_http_client_timeout_option(self) -> None: # explicitly passing the default timeout currently results in it being ignored with httpx.Client(timeout=HTTPX_DEFAULT_TIMEOUT) as http_client: client = Stagehand( - base_url=base_url, api_key=api_key, _strict_response_validation=True, http_client=http_client + base_url=base_url, + browserbase_api_key=browserbase_api_key, + browserbase_project_id=browserbase_project_id, + model_api_key=model_api_key, + _strict_response_validation=True, + http_client=http_client, ) request = client._build_request(FinalRequestOptions(method="get", url="/foo")) @@ -317,14 +357,21 @@ async def test_invalid_http_client(self) -> None: async with httpx.AsyncClient() as http_client: Stagehand( base_url=base_url, - api_key=api_key, + browserbase_api_key=browserbase_api_key, + browserbase_project_id=browserbase_project_id, + model_api_key=model_api_key, _strict_response_validation=True, http_client=cast(Any, http_client), ) def test_default_headers_option(self) -> None: test_client = Stagehand( - base_url=base_url, api_key=api_key, _strict_response_validation=True, default_headers={"X-Foo": "bar"} + base_url=base_url, + browserbase_api_key=browserbase_api_key, + browserbase_project_id=browserbase_project_id, + model_api_key=model_api_key, + _strict_response_validation=True, + default_headers={"X-Foo": "bar"}, ) request = test_client._build_request(FinalRequestOptions(method="get", url="/foo")) assert request.headers.get("x-foo") == "bar" @@ -332,7 +379,9 @@ def test_default_headers_option(self) -> None: test_client2 = Stagehand( base_url=base_url, - api_key=api_key, + browserbase_api_key=browserbase_api_key, + browserbase_project_id=browserbase_project_id, + model_api_key=model_api_key, _strict_response_validation=True, default_headers={ "X-Foo": "stainless", @@ -347,18 +396,45 @@ def test_default_headers_option(self) -> None: test_client2.close() def test_validate_headers(self) -> None: - client = Stagehand(base_url=base_url, api_key=api_key, _strict_response_validation=True) + client = Stagehand( + base_url=base_url, + browserbase_api_key=browserbase_api_key, + browserbase_project_id=browserbase_project_id, + model_api_key=model_api_key, + _strict_response_validation=True, + ) + request = client._build_request(FinalRequestOptions(method="get", url="/foo")) + assert request.headers.get("x-bb-api-key") == browserbase_api_key + request = client._build_request(FinalRequestOptions(method="get", url="/foo")) + assert request.headers.get("x-bb-project-id") == browserbase_project_id request = client._build_request(FinalRequestOptions(method="get", url="/foo")) - assert request.headers.get("Authorization") == f"Bearer {api_key}" + assert request.headers.get("x-model-api-key") == model_api_key with pytest.raises(StagehandError): - with update_env(**{"STAGEHAND_API_KEY": Omit()}): - client2 = Stagehand(base_url=base_url, api_key=None, _strict_response_validation=True) + with update_env( + **{ + "BROWSERBASE_API_KEY": Omit(), + "BROWSERBASE_PROJECT_ID": Omit(), + "MODEL_API_KEY": Omit(), + } + ): + client2 = Stagehand( + base_url=base_url, + browserbase_api_key=None, + browserbase_project_id=None, + model_api_key=None, + _strict_response_validation=True, + ) _ = client2 def test_default_query_option(self) -> None: client = Stagehand( - base_url=base_url, api_key=api_key, _strict_response_validation=True, default_query={"query_param": "bar"} + base_url=base_url, + browserbase_api_key=browserbase_api_key, + browserbase_project_id=browserbase_project_id, + model_api_key=model_api_key, + _strict_response_validation=True, + default_query={"query_param": "bar"}, ) request = client._build_request(FinalRequestOptions(method="get", url="/foo")) url = httpx.URL(request.url) @@ -560,7 +636,13 @@ class Model(BaseModel): assert response.foo == 2 def test_base_url_setter(self) -> None: - client = Stagehand(base_url="https://example.com/from_init", api_key=api_key, _strict_response_validation=True) + client = Stagehand( + base_url="https://example.com/from_init", + browserbase_api_key=browserbase_api_key, + browserbase_project_id=browserbase_project_id, + model_api_key=model_api_key, + _strict_response_validation=True, + ) assert client.base_url == "https://example.com/from_init/" client.base_url = "https://example.com/from_setter" # type: ignore[assignment] @@ -571,28 +653,29 @@ def test_base_url_setter(self) -> None: def test_base_url_env(self) -> None: with update_env(STAGEHAND_BASE_URL="http://localhost:5000/from/env"): - client = Stagehand(api_key=api_key, _strict_response_validation=True) - assert client.base_url == "http://localhost:5000/from/env/" - - # explicit environment arg requires explicitness - with update_env(STAGEHAND_BASE_URL="http://localhost:5000/from/env"): - with pytest.raises(ValueError, match=r"you must pass base_url=None"): - Stagehand(api_key=api_key, _strict_response_validation=True, environment="production") - client = Stagehand( - base_url=None, api_key=api_key, _strict_response_validation=True, environment="production" + browserbase_api_key=browserbase_api_key, + browserbase_project_id=browserbase_project_id, + model_api_key=model_api_key, + _strict_response_validation=True, ) - assert str(client.base_url).startswith("https://api.stagehand.browserbase.com/v1") - - client.close() + assert client.base_url == "http://localhost:5000/from/env/" @pytest.mark.parametrize( "client", [ - Stagehand(base_url="http://localhost:5000/custom/path/", api_key=api_key, _strict_response_validation=True), Stagehand( base_url="http://localhost:5000/custom/path/", - api_key=api_key, + browserbase_api_key=browserbase_api_key, + browserbase_project_id=browserbase_project_id, + model_api_key=model_api_key, + _strict_response_validation=True, + ), + Stagehand( + base_url="http://localhost:5000/custom/path/", + browserbase_api_key=browserbase_api_key, + browserbase_project_id=browserbase_project_id, + model_api_key=model_api_key, _strict_response_validation=True, http_client=httpx.Client(), ), @@ -613,10 +696,18 @@ def test_base_url_trailing_slash(self, client: Stagehand) -> None: @pytest.mark.parametrize( "client", [ - Stagehand(base_url="http://localhost:5000/custom/path/", api_key=api_key, _strict_response_validation=True), Stagehand( base_url="http://localhost:5000/custom/path/", - api_key=api_key, + browserbase_api_key=browserbase_api_key, + browserbase_project_id=browserbase_project_id, + model_api_key=model_api_key, + _strict_response_validation=True, + ), + Stagehand( + base_url="http://localhost:5000/custom/path/", + browserbase_api_key=browserbase_api_key, + browserbase_project_id=browserbase_project_id, + model_api_key=model_api_key, _strict_response_validation=True, http_client=httpx.Client(), ), @@ -637,10 +728,18 @@ def test_base_url_no_trailing_slash(self, client: Stagehand) -> None: @pytest.mark.parametrize( "client", [ - Stagehand(base_url="http://localhost:5000/custom/path/", api_key=api_key, _strict_response_validation=True), Stagehand( base_url="http://localhost:5000/custom/path/", - api_key=api_key, + browserbase_api_key=browserbase_api_key, + browserbase_project_id=browserbase_project_id, + model_api_key=model_api_key, + _strict_response_validation=True, + ), + Stagehand( + base_url="http://localhost:5000/custom/path/", + browserbase_api_key=browserbase_api_key, + browserbase_project_id=browserbase_project_id, + model_api_key=model_api_key, _strict_response_validation=True, http_client=httpx.Client(), ), @@ -659,7 +758,13 @@ def test_absolute_request_url(self, client: Stagehand) -> None: client.close() def test_copied_client_does_not_close_http(self) -> None: - test_client = Stagehand(base_url=base_url, api_key=api_key, _strict_response_validation=True) + test_client = Stagehand( + base_url=base_url, + browserbase_api_key=browserbase_api_key, + browserbase_project_id=browserbase_project_id, + model_api_key=model_api_key, + _strict_response_validation=True, + ) assert not test_client.is_closed() copied = test_client.copy() @@ -670,7 +775,13 @@ def test_copied_client_does_not_close_http(self) -> None: assert not test_client.is_closed() def test_client_context_manager(self) -> None: - test_client = Stagehand(base_url=base_url, api_key=api_key, _strict_response_validation=True) + test_client = Stagehand( + base_url=base_url, + browserbase_api_key=browserbase_api_key, + browserbase_project_id=browserbase_project_id, + model_api_key=model_api_key, + _strict_response_validation=True, + ) with test_client as c2: assert c2 is test_client assert not c2.is_closed() @@ -691,7 +802,14 @@ class Model(BaseModel): def test_client_max_retries_validation(self) -> None: with pytest.raises(TypeError, match=r"max_retries cannot be None"): - Stagehand(base_url=base_url, api_key=api_key, _strict_response_validation=True, max_retries=cast(Any, None)) + Stagehand( + base_url=base_url, + browserbase_api_key=browserbase_api_key, + browserbase_project_id=browserbase_project_id, + model_api_key=model_api_key, + _strict_response_validation=True, + max_retries=cast(Any, None), + ) @pytest.mark.respx(base_url=base_url) def test_received_text_for_expected_json(self, respx_mock: MockRouter) -> None: @@ -700,12 +818,24 @@ class Model(BaseModel): respx_mock.get("/foo").mock(return_value=httpx.Response(200, text="my-custom-format")) - strict_client = Stagehand(base_url=base_url, api_key=api_key, _strict_response_validation=True) + strict_client = Stagehand( + base_url=base_url, + browserbase_api_key=browserbase_api_key, + browserbase_project_id=browserbase_project_id, + model_api_key=model_api_key, + _strict_response_validation=True, + ) with pytest.raises(APIResponseValidationError): strict_client.get("/foo", cast_to=Model) - non_strict_client = Stagehand(base_url=base_url, api_key=api_key, _strict_response_validation=False) + non_strict_client = Stagehand( + base_url=base_url, + browserbase_api_key=browserbase_api_key, + browserbase_project_id=browserbase_project_id, + model_api_key=model_api_key, + _strict_response_validation=False, + ) response = non_strict_client.get("/foo", cast_to=Model) assert isinstance(response, str) # type: ignore[unreachable] @@ -926,9 +1056,17 @@ def test_copy(self, async_client: AsyncStagehand) -> None: copied = async_client.copy() assert id(copied) != id(async_client) - copied = async_client.copy(api_key="another My API Key") - assert copied.api_key == "another My API Key" - assert async_client.api_key == "My API Key" + copied = async_client.copy(browserbase_api_key="another My Browserbase API Key") + assert copied.browserbase_api_key == "another My Browserbase API Key" + assert async_client.browserbase_api_key == "My Browserbase API Key" + + copied = async_client.copy(browserbase_project_id="another My Browserbase Project ID") + assert copied.browserbase_project_id == "another My Browserbase Project ID" + assert async_client.browserbase_project_id == "My Browserbase Project ID" + + copied = async_client.copy(model_api_key="another My Model API Key") + assert copied.model_api_key == "another My Model API Key" + assert async_client.model_api_key == "My Model API Key" def test_copy_default_options(self, async_client: AsyncStagehand) -> None: # options that have a default are overridden correctly @@ -948,7 +1086,12 @@ def test_copy_default_options(self, async_client: AsyncStagehand) -> None: async def test_copy_default_headers(self) -> None: client = AsyncStagehand( - base_url=base_url, api_key=api_key, _strict_response_validation=True, default_headers={"X-Foo": "bar"} + base_url=base_url, + browserbase_api_key=browserbase_api_key, + browserbase_project_id=browserbase_project_id, + model_api_key=model_api_key, + _strict_response_validation=True, + default_headers={"X-Foo": "bar"}, ) assert client.default_headers["X-Foo"] == "bar" @@ -983,7 +1126,12 @@ async def test_copy_default_headers(self) -> None: async def test_copy_default_query(self) -> None: client = AsyncStagehand( - base_url=base_url, api_key=api_key, _strict_response_validation=True, default_query={"foo": "bar"} + base_url=base_url, + browserbase_api_key=browserbase_api_key, + browserbase_project_id=browserbase_project_id, + model_api_key=model_api_key, + _strict_response_validation=True, + default_query={"foo": "bar"}, ) assert _get_params(client)["foo"] == "bar" @@ -1111,7 +1259,12 @@ async def test_request_timeout(self, async_client: AsyncStagehand) -> None: async def test_client_timeout_option(self) -> None: client = AsyncStagehand( - base_url=base_url, api_key=api_key, _strict_response_validation=True, timeout=httpx.Timeout(0) + base_url=base_url, + browserbase_api_key=browserbase_api_key, + browserbase_project_id=browserbase_project_id, + model_api_key=model_api_key, + _strict_response_validation=True, + timeout=httpx.Timeout(0), ) request = client._build_request(FinalRequestOptions(method="get", url="/foo")) @@ -1124,7 +1277,12 @@ async def test_http_client_timeout_option(self) -> None: # custom timeout given to the httpx client should be used async with httpx.AsyncClient(timeout=None) as http_client: client = AsyncStagehand( - base_url=base_url, api_key=api_key, _strict_response_validation=True, http_client=http_client + base_url=base_url, + browserbase_api_key=browserbase_api_key, + browserbase_project_id=browserbase_project_id, + model_api_key=model_api_key, + _strict_response_validation=True, + http_client=http_client, ) request = client._build_request(FinalRequestOptions(method="get", url="/foo")) @@ -1136,7 +1294,12 @@ async def test_http_client_timeout_option(self) -> None: # no timeout given to the httpx client should not use the httpx default async with httpx.AsyncClient() as http_client: client = AsyncStagehand( - base_url=base_url, api_key=api_key, _strict_response_validation=True, http_client=http_client + base_url=base_url, + browserbase_api_key=browserbase_api_key, + browserbase_project_id=browserbase_project_id, + model_api_key=model_api_key, + _strict_response_validation=True, + http_client=http_client, ) request = client._build_request(FinalRequestOptions(method="get", url="/foo")) @@ -1148,7 +1311,12 @@ async def test_http_client_timeout_option(self) -> None: # explicitly passing the default timeout currently results in it being ignored async with httpx.AsyncClient(timeout=HTTPX_DEFAULT_TIMEOUT) as http_client: client = AsyncStagehand( - base_url=base_url, api_key=api_key, _strict_response_validation=True, http_client=http_client + base_url=base_url, + browserbase_api_key=browserbase_api_key, + browserbase_project_id=browserbase_project_id, + model_api_key=model_api_key, + _strict_response_validation=True, + http_client=http_client, ) request = client._build_request(FinalRequestOptions(method="get", url="/foo")) @@ -1162,14 +1330,21 @@ def test_invalid_http_client(self) -> None: with httpx.Client() as http_client: AsyncStagehand( base_url=base_url, - api_key=api_key, + browserbase_api_key=browserbase_api_key, + browserbase_project_id=browserbase_project_id, + model_api_key=model_api_key, _strict_response_validation=True, http_client=cast(Any, http_client), ) async def test_default_headers_option(self) -> None: test_client = AsyncStagehand( - base_url=base_url, api_key=api_key, _strict_response_validation=True, default_headers={"X-Foo": "bar"} + base_url=base_url, + browserbase_api_key=browserbase_api_key, + browserbase_project_id=browserbase_project_id, + model_api_key=model_api_key, + _strict_response_validation=True, + default_headers={"X-Foo": "bar"}, ) request = test_client._build_request(FinalRequestOptions(method="get", url="/foo")) assert request.headers.get("x-foo") == "bar" @@ -1177,7 +1352,9 @@ async def test_default_headers_option(self) -> None: test_client2 = AsyncStagehand( base_url=base_url, - api_key=api_key, + browserbase_api_key=browserbase_api_key, + browserbase_project_id=browserbase_project_id, + model_api_key=model_api_key, _strict_response_validation=True, default_headers={ "X-Foo": "stainless", @@ -1192,18 +1369,45 @@ async def test_default_headers_option(self) -> None: await test_client2.close() def test_validate_headers(self) -> None: - client = AsyncStagehand(base_url=base_url, api_key=api_key, _strict_response_validation=True) + client = AsyncStagehand( + base_url=base_url, + browserbase_api_key=browserbase_api_key, + browserbase_project_id=browserbase_project_id, + model_api_key=model_api_key, + _strict_response_validation=True, + ) request = client._build_request(FinalRequestOptions(method="get", url="/foo")) - assert request.headers.get("Authorization") == f"Bearer {api_key}" + assert request.headers.get("x-bb-api-key") == browserbase_api_key + request = client._build_request(FinalRequestOptions(method="get", url="/foo")) + assert request.headers.get("x-bb-project-id") == browserbase_project_id + request = client._build_request(FinalRequestOptions(method="get", url="/foo")) + assert request.headers.get("x-model-api-key") == model_api_key with pytest.raises(StagehandError): - with update_env(**{"STAGEHAND_API_KEY": Omit()}): - client2 = AsyncStagehand(base_url=base_url, api_key=None, _strict_response_validation=True) + with update_env( + **{ + "BROWSERBASE_API_KEY": Omit(), + "BROWSERBASE_PROJECT_ID": Omit(), + "MODEL_API_KEY": Omit(), + } + ): + client2 = AsyncStagehand( + base_url=base_url, + browserbase_api_key=None, + browserbase_project_id=None, + model_api_key=None, + _strict_response_validation=True, + ) _ = client2 async def test_default_query_option(self) -> None: client = AsyncStagehand( - base_url=base_url, api_key=api_key, _strict_response_validation=True, default_query={"query_param": "bar"} + base_url=base_url, + browserbase_api_key=browserbase_api_key, + browserbase_project_id=browserbase_project_id, + model_api_key=model_api_key, + _strict_response_validation=True, + default_query={"query_param": "bar"}, ) request = client._build_request(FinalRequestOptions(method="get", url="/foo")) url = httpx.URL(request.url) @@ -1408,7 +1612,11 @@ class Model(BaseModel): async def test_base_url_setter(self) -> None: client = AsyncStagehand( - base_url="https://example.com/from_init", api_key=api_key, _strict_response_validation=True + base_url="https://example.com/from_init", + browserbase_api_key=browserbase_api_key, + browserbase_project_id=browserbase_project_id, + model_api_key=model_api_key, + _strict_response_validation=True, ) assert client.base_url == "https://example.com/from_init/" @@ -1420,30 +1628,29 @@ async def test_base_url_setter(self) -> None: async def test_base_url_env(self) -> None: with update_env(STAGEHAND_BASE_URL="http://localhost:5000/from/env"): - client = AsyncStagehand(api_key=api_key, _strict_response_validation=True) - assert client.base_url == "http://localhost:5000/from/env/" - - # explicit environment arg requires explicitness - with update_env(STAGEHAND_BASE_URL="http://localhost:5000/from/env"): - with pytest.raises(ValueError, match=r"you must pass base_url=None"): - AsyncStagehand(api_key=api_key, _strict_response_validation=True, environment="production") - client = AsyncStagehand( - base_url=None, api_key=api_key, _strict_response_validation=True, environment="production" + browserbase_api_key=browserbase_api_key, + browserbase_project_id=browserbase_project_id, + model_api_key=model_api_key, + _strict_response_validation=True, ) - assert str(client.base_url).startswith("https://api.stagehand.browserbase.com/v1") - - await client.close() + assert client.base_url == "http://localhost:5000/from/env/" @pytest.mark.parametrize( "client", [ AsyncStagehand( - base_url="http://localhost:5000/custom/path/", api_key=api_key, _strict_response_validation=True + base_url="http://localhost:5000/custom/path/", + browserbase_api_key=browserbase_api_key, + browserbase_project_id=browserbase_project_id, + model_api_key=model_api_key, + _strict_response_validation=True, ), AsyncStagehand( base_url="http://localhost:5000/custom/path/", - api_key=api_key, + browserbase_api_key=browserbase_api_key, + browserbase_project_id=browserbase_project_id, + model_api_key=model_api_key, _strict_response_validation=True, http_client=httpx.AsyncClient(), ), @@ -1465,11 +1672,17 @@ async def test_base_url_trailing_slash(self, client: AsyncStagehand) -> None: "client", [ AsyncStagehand( - base_url="http://localhost:5000/custom/path/", api_key=api_key, _strict_response_validation=True + base_url="http://localhost:5000/custom/path/", + browserbase_api_key=browserbase_api_key, + browserbase_project_id=browserbase_project_id, + model_api_key=model_api_key, + _strict_response_validation=True, ), AsyncStagehand( base_url="http://localhost:5000/custom/path/", - api_key=api_key, + browserbase_api_key=browserbase_api_key, + browserbase_project_id=browserbase_project_id, + model_api_key=model_api_key, _strict_response_validation=True, http_client=httpx.AsyncClient(), ), @@ -1491,11 +1704,17 @@ async def test_base_url_no_trailing_slash(self, client: AsyncStagehand) -> None: "client", [ AsyncStagehand( - base_url="http://localhost:5000/custom/path/", api_key=api_key, _strict_response_validation=True + base_url="http://localhost:5000/custom/path/", + browserbase_api_key=browserbase_api_key, + browserbase_project_id=browserbase_project_id, + model_api_key=model_api_key, + _strict_response_validation=True, ), AsyncStagehand( base_url="http://localhost:5000/custom/path/", - api_key=api_key, + browserbase_api_key=browserbase_api_key, + browserbase_project_id=browserbase_project_id, + model_api_key=model_api_key, _strict_response_validation=True, http_client=httpx.AsyncClient(), ), @@ -1514,7 +1733,13 @@ async def test_absolute_request_url(self, client: AsyncStagehand) -> None: await client.close() async def test_copied_client_does_not_close_http(self) -> None: - test_client = AsyncStagehand(base_url=base_url, api_key=api_key, _strict_response_validation=True) + test_client = AsyncStagehand( + base_url=base_url, + browserbase_api_key=browserbase_api_key, + browserbase_project_id=browserbase_project_id, + model_api_key=model_api_key, + _strict_response_validation=True, + ) assert not test_client.is_closed() copied = test_client.copy() @@ -1526,7 +1751,13 @@ async def test_copied_client_does_not_close_http(self) -> None: assert not test_client.is_closed() async def test_client_context_manager(self) -> None: - test_client = AsyncStagehand(base_url=base_url, api_key=api_key, _strict_response_validation=True) + test_client = AsyncStagehand( + base_url=base_url, + browserbase_api_key=browserbase_api_key, + browserbase_project_id=browserbase_project_id, + model_api_key=model_api_key, + _strict_response_validation=True, + ) async with test_client as c2: assert c2 is test_client assert not c2.is_closed() @@ -1548,7 +1779,12 @@ class Model(BaseModel): async def test_client_max_retries_validation(self) -> None: with pytest.raises(TypeError, match=r"max_retries cannot be None"): AsyncStagehand( - base_url=base_url, api_key=api_key, _strict_response_validation=True, max_retries=cast(Any, None) + base_url=base_url, + browserbase_api_key=browserbase_api_key, + browserbase_project_id=browserbase_project_id, + model_api_key=model_api_key, + _strict_response_validation=True, + max_retries=cast(Any, None), ) @pytest.mark.respx(base_url=base_url) @@ -1558,12 +1794,24 @@ class Model(BaseModel): respx_mock.get("/foo").mock(return_value=httpx.Response(200, text="my-custom-format")) - strict_client = AsyncStagehand(base_url=base_url, api_key=api_key, _strict_response_validation=True) + strict_client = AsyncStagehand( + base_url=base_url, + browserbase_api_key=browserbase_api_key, + browserbase_project_id=browserbase_project_id, + model_api_key=model_api_key, + _strict_response_validation=True, + ) with pytest.raises(APIResponseValidationError): await strict_client.get("/foo", cast_to=Model) - non_strict_client = AsyncStagehand(base_url=base_url, api_key=api_key, _strict_response_validation=False) + non_strict_client = AsyncStagehand( + base_url=base_url, + browserbase_api_key=browserbase_api_key, + browserbase_project_id=browserbase_project_id, + model_api_key=model_api_key, + _strict_response_validation=False, + ) response = await non_strict_client.get("/foo", cast_to=Model) assert isinstance(response, str) # type: ignore[unreachable] From f5790bcaa443668f3c9e2f449b70190ac4a81ed1 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 16 Dec 2025 20:12:11 +0000 Subject: [PATCH 21/88] feat(api): manual updates --- .stats.yml | 2 +- src/stagehand/_client.py | 16 ++++++++++------ 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/.stats.yml b/.stats.yml index 1e160655..44d6d131 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 7 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fstagehand-3607b588cab78536eb7de9f6acffe8ddda1d34aebe5910c2147421aa6c16bf22.yml openapi_spec_hash: fb507e8d38b4978a5717fbb144197868 -config_hash: fc6606301b5142487a69d296f154b265 +config_hash: 9d54b9fd851ec7ac25b85f579be64425 diff --git a/src/stagehand/_client.py b/src/stagehand/_client.py index 6669ffdb..a8767a89 100644 --- a/src/stagehand/_client.py +++ b/src/stagehand/_client.py @@ -50,7 +50,7 @@ class Stagehand(SyncAPIClient): # client options browserbase_api_key: str browserbase_project_id: str - model_api_key: str | None + model_api_key: str def __init__( self, @@ -102,6 +102,10 @@ def __init__( if model_api_key is None: model_api_key = os.environ.get("MODEL_API_KEY") + if model_api_key is None: + raise StagehandError( + "The model_api_key client option must be set either by passing model_api_key to the client or by setting the MODEL_API_KEY environment variable" + ) self.model_api_key = model_api_key if base_url is None: @@ -147,8 +151,6 @@ def _bb_project_id_auth(self) -> dict[str, str]: @property def _llm_model_api_key_auth(self) -> dict[str, str]: model_api_key = self.model_api_key - if model_api_key is None: - return {} return {"x-model-api-key": model_api_key} @property @@ -257,7 +259,7 @@ class AsyncStagehand(AsyncAPIClient): # client options browserbase_api_key: str browserbase_project_id: str - model_api_key: str | None + model_api_key: str def __init__( self, @@ -309,6 +311,10 @@ def __init__( if model_api_key is None: model_api_key = os.environ.get("MODEL_API_KEY") + if model_api_key is None: + raise StagehandError( + "The model_api_key client option must be set either by passing model_api_key to the client or by setting the MODEL_API_KEY environment variable" + ) self.model_api_key = model_api_key if base_url is None: @@ -354,8 +360,6 @@ def _bb_project_id_auth(self) -> dict[str, str]: @property def _llm_model_api_key_auth(self) -> dict[str, str]: model_api_key = self.model_api_key - if model_api_key is None: - return {} return {"x-model-api-key": model_api_key} @property From 186c9500e2e19f3ae913d62967889702c429a421 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 16 Dec 2025 20:24:04 +0000 Subject: [PATCH 22/88] codegen metadata --- .stats.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.stats.yml b/.stats.yml index 44d6d131..91b86f30 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 7 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fstagehand-3607b588cab78536eb7de9f6acffe8ddda1d34aebe5910c2147421aa6c16bf22.yml -openapi_spec_hash: fb507e8d38b4978a5717fbb144197868 -config_hash: 9d54b9fd851ec7ac25b85f579be64425 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fstagehand-2c88c6d890406ff8a5f1bca692264fb9af4bc4fe64df0986e06d3386fc6d6fcb.yml +openapi_spec_hash: dc6ea17f8152708dc0a390c7f86b1a5d +config_hash: b01f15c540ab2c92808c2bba96368631 From a9cc1de160757e04dbe9a777d7efa44026e569ab Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 16 Dec 2025 20:33:47 +0000 Subject: [PATCH 23/88] feat(api): manual updates --- .stats.yml | 2 +- README.md | 26 +++++++++++++------------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/.stats.yml b/.stats.yml index 91b86f30..35f1aef4 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 7 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fstagehand-2c88c6d890406ff8a5f1bca692264fb9af4bc4fe64df0986e06d3386fc6d6fcb.yml openapi_spec_hash: dc6ea17f8152708dc0a390c7f86b1a5d -config_hash: b01f15c540ab2c92808c2bba96368631 +config_hash: bdeeac521c2cf3846aec9f75cb681d97 diff --git a/README.md b/README.md index b87f9fc1..7550f42c 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,7 @@ client = Stagehand( ) response = client.sessions.act( - session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + session_id="00000000-your-session-id-000000000000", input="click the first link on the page", ) print(response.actions) @@ -75,7 +75,7 @@ client = AsyncStagehand( async def main() -> None: response = await client.sessions.act( - session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + session_id="00000000-your-session-id-000000000000", input="click the first link on the page", ) print(response.actions) @@ -118,7 +118,7 @@ async def main() -> None: http_client=DefaultAioHttpClient(), ) as client: response = await client.sessions.act( - session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + session_id="00000000-your-session-id-000000000000", input="click the first link on the page", ) print(response.actions) @@ -170,8 +170,8 @@ client = Stagehand() try: client.sessions.start( - browserbase_api_key="BROWSERBASE_API_KEY", - browserbase_project_id="BROWSERBASE_PROJECT_ID", + browserbase_api_key="", + browserbase_project_id="", ) except stagehand.APIConnectionError as e: print("The server could not be reached") @@ -216,8 +216,8 @@ client = Stagehand( # Or, configure per-request: client.with_options(max_retries=5).sessions.start( - browserbase_api_key="BROWSERBASE_API_KEY", - browserbase_project_id="BROWSERBASE_PROJECT_ID", + browserbase_api_key="", + browserbase_project_id="", ) ``` @@ -242,8 +242,8 @@ client = Stagehand( # Override per-request: client.with_options(timeout=5.0).sessions.start( - browserbase_api_key="BROWSERBASE_API_KEY", - browserbase_project_id="BROWSERBASE_PROJECT_ID", + browserbase_api_key="", + browserbase_project_id="", ) ``` @@ -286,8 +286,8 @@ from stagehand import Stagehand client = Stagehand() response = client.sessions.with_raw_response.start( - browserbase_api_key="BROWSERBASE_API_KEY", - browserbase_project_id="BROWSERBASE_PROJECT_ID", + browserbase_api_key="", + browserbase_project_id="", ) print(response.headers.get('X-My-Header')) @@ -307,8 +307,8 @@ To stream the response body, use `.with_streaming_response` instead, which requi ```python with client.sessions.with_streaming_response.start( - browserbase_api_key="BROWSERBASE_API_KEY", - browserbase_project_id="BROWSERBASE_PROJECT_ID", + browserbase_api_key="", + browserbase_project_id="", ) as response: print(response.headers.get("X-My-Header")) From 86653a29642d890cfd251ffdf87d3546147f7bfd Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 16 Dec 2025 21:15:56 +0000 Subject: [PATCH 24/88] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index 35f1aef4..d9fd06e9 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 7 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fstagehand-2c88c6d890406ff8a5f1bca692264fb9af4bc4fe64df0986e06d3386fc6d6fcb.yml openapi_spec_hash: dc6ea17f8152708dc0a390c7f86b1a5d -config_hash: bdeeac521c2cf3846aec9f75cb681d97 +config_hash: a17b6052ac65237b7b8e145f4f692d3c From 882e8ad3bfccb2ffd991c69353563f926542b416 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 16 Dec 2025 22:21:23 +0000 Subject: [PATCH 25/88] feat(api): manual updates --- .stats.yml | 6 +++--- README.md | 20 +++++++++---------- .../types/session_execute_agent_response.py | 5 +---- 3 files changed, 14 insertions(+), 17 deletions(-) diff --git a/.stats.yml b/.stats.yml index d9fd06e9..4a478683 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 7 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fstagehand-2c88c6d890406ff8a5f1bca692264fb9af4bc4fe64df0986e06d3386fc6d6fcb.yml -openapi_spec_hash: dc6ea17f8152708dc0a390c7f86b1a5d -config_hash: a17b6052ac65237b7b8e145f4f692d3c +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fstagehand-e52e8d3513159931200e5d4bd32cfdc475fe6e7db6b8015c394f5625b5a3ce1f.yml +openapi_spec_hash: fa597eb985cd1c3ba03acf68d6416857 +config_hash: b138dc33d4d0880499266048be517958 diff --git a/README.md b/README.md index 7550f42c..74c28337 100644 --- a/README.md +++ b/README.md @@ -170,8 +170,8 @@ client = Stagehand() try: client.sessions.start( - browserbase_api_key="", - browserbase_project_id="", + browserbase_api_key="BROWSERBASE_API_KEY", + browserbase_project_id="BROWSERBASE_PROJECT_ID", ) except stagehand.APIConnectionError as e: print("The server could not be reached") @@ -216,8 +216,8 @@ client = Stagehand( # Or, configure per-request: client.with_options(max_retries=5).sessions.start( - browserbase_api_key="", - browserbase_project_id="", + browserbase_api_key="BROWSERBASE_API_KEY", + browserbase_project_id="BROWSERBASE_PROJECT_ID", ) ``` @@ -242,8 +242,8 @@ client = Stagehand( # Override per-request: client.with_options(timeout=5.0).sessions.start( - browserbase_api_key="", - browserbase_project_id="", + browserbase_api_key="BROWSERBASE_API_KEY", + browserbase_project_id="BROWSERBASE_PROJECT_ID", ) ``` @@ -286,8 +286,8 @@ from stagehand import Stagehand client = Stagehand() response = client.sessions.with_raw_response.start( - browserbase_api_key="", - browserbase_project_id="", + browserbase_api_key="BROWSERBASE_API_KEY", + browserbase_project_id="BROWSERBASE_PROJECT_ID", ) print(response.headers.get('X-My-Header')) @@ -307,8 +307,8 @@ To stream the response body, use `.with_streaming_response` instead, which requi ```python with client.sessions.with_streaming_response.start( - browserbase_api_key="", - browserbase_project_id="", + browserbase_api_key="BROWSERBASE_API_KEY", + browserbase_project_id="BROWSERBASE_PROJECT_ID", ) as response: print(response.headers.get("X-My-Header")) diff --git a/src/stagehand/types/session_execute_agent_response.py b/src/stagehand/types/session_execute_agent_response.py index 3fa53390..80d0c0b2 100644 --- a/src/stagehand/types/session_execute_agent_response.py +++ b/src/stagehand/types/session_execute_agent_response.py @@ -1,6 +1,6 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -from typing import List, Optional +from typing import Optional from .._models import BaseModel @@ -10,6 +10,3 @@ class SessionExecuteAgentResponse(BaseModel): message: Optional[str] = None """Final message from the agent""" - - steps: Optional[List[object]] = None - """Steps taken by the agent""" From 2e40d2180ff61b462d64460aba08fe675c597d50 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 16 Dec 2025 22:23:05 +0000 Subject: [PATCH 26/88] feat(api): manual updates --- .stats.yml | 6 +++--- README.md | 20 ++++++++++---------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/.stats.yml b/.stats.yml index 4a478683..36782ae7 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 7 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fstagehand-e52e8d3513159931200e5d4bd32cfdc475fe6e7db6b8015c394f5625b5a3ce1f.yml -openapi_spec_hash: fa597eb985cd1c3ba03acf68d6416857 -config_hash: b138dc33d4d0880499266048be517958 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fstagehand-705638ac8966569986bd9ebb7c9761bf0016909e9f2753e77ceabb12c8049511.yml +openapi_spec_hash: a8fbbcaa38e91c7f97313620b42d8d62 +config_hash: a35b56eb05306a0f02e83c11d57f975f diff --git a/README.md b/README.md index 74c28337..b837b961 100644 --- a/README.md +++ b/README.md @@ -170,8 +170,8 @@ client = Stagehand() try: client.sessions.start( - browserbase_api_key="BROWSERBASE_API_KEY", - browserbase_project_id="BROWSERBASE_PROJECT_ID", + browserbase_api_key="your Browserbase API key", + browserbase_project_id="your Browserbase Project ID", ) except stagehand.APIConnectionError as e: print("The server could not be reached") @@ -216,8 +216,8 @@ client = Stagehand( # Or, configure per-request: client.with_options(max_retries=5).sessions.start( - browserbase_api_key="BROWSERBASE_API_KEY", - browserbase_project_id="BROWSERBASE_PROJECT_ID", + browserbase_api_key="your Browserbase API key", + browserbase_project_id="your Browserbase Project ID", ) ``` @@ -242,8 +242,8 @@ client = Stagehand( # Override per-request: client.with_options(timeout=5.0).sessions.start( - browserbase_api_key="BROWSERBASE_API_KEY", - browserbase_project_id="BROWSERBASE_PROJECT_ID", + browserbase_api_key="your Browserbase API key", + browserbase_project_id="your Browserbase Project ID", ) ``` @@ -286,8 +286,8 @@ from stagehand import Stagehand client = Stagehand() response = client.sessions.with_raw_response.start( - browserbase_api_key="BROWSERBASE_API_KEY", - browserbase_project_id="BROWSERBASE_PROJECT_ID", + browserbase_api_key="your Browserbase API key", + browserbase_project_id="your Browserbase Project ID", ) print(response.headers.get('X-My-Header')) @@ -307,8 +307,8 @@ To stream the response body, use `.with_streaming_response` instead, which requi ```python with client.sessions.with_streaming_response.start( - browserbase_api_key="BROWSERBASE_API_KEY", - browserbase_project_id="BROWSERBASE_PROJECT_ID", + browserbase_api_key="your Browserbase API key", + browserbase_project_id="your Browserbase Project ID", ) as response: print(response.headers.get("X-My-Header")) From f93026bc586553cc50f430687cb46e0c01f59dff Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 17 Dec 2025 02:13:44 +0000 Subject: [PATCH 27/88] feat(api): manual updates --- .stats.yml | 6 +- README.md | 62 +- api.md | 20 +- src/stagehand/resources/sessions.py | 821 +----------------- src/stagehand/types/__init__.py | 15 - src/stagehand/types/action.py | 26 - src/stagehand/types/action_param.py | 27 - src/stagehand/types/model_config_param.py | 22 - src/stagehand/types/session_act_params.py | 37 - src/stagehand/types/session_act_response.py | 19 - src/stagehand/types/session_end_response.py | 11 - .../types/session_execute_agent_params.py | 46 - .../types/session_execute_agent_response.py | 12 - src/stagehand/types/session_extract_params.py | 35 - .../types/session_extract_response.py | 17 - .../types/session_navigate_params.py | 25 - .../types/session_navigate_response.py | 17 - src/stagehand/types/session_observe_params.py | 31 - .../types/session_observe_response.py | 10 - src/stagehand/types/session_start_params.py | 23 +- src/stagehand/types/session_start_response.py | 15 - tests/api_resources/test_sessions.py | 811 +---------------- tests/test_client.py | 48 +- 23 files changed, 90 insertions(+), 2066 deletions(-) delete mode 100644 src/stagehand/types/action.py delete mode 100644 src/stagehand/types/action_param.py delete mode 100644 src/stagehand/types/model_config_param.py delete mode 100644 src/stagehand/types/session_act_params.py delete mode 100644 src/stagehand/types/session_act_response.py delete mode 100644 src/stagehand/types/session_end_response.py delete mode 100644 src/stagehand/types/session_execute_agent_params.py delete mode 100644 src/stagehand/types/session_execute_agent_response.py delete mode 100644 src/stagehand/types/session_extract_params.py delete mode 100644 src/stagehand/types/session_extract_response.py delete mode 100644 src/stagehand/types/session_navigate_params.py delete mode 100644 src/stagehand/types/session_navigate_response.py delete mode 100644 src/stagehand/types/session_observe_params.py delete mode 100644 src/stagehand/types/session_observe_response.py delete mode 100644 src/stagehand/types/session_start_response.py diff --git a/.stats.yml b/.stats.yml index 36782ae7..f5e93e0f 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ -configured_endpoints: 7 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fstagehand-705638ac8966569986bd9ebb7c9761bf0016909e9f2753e77ceabb12c8049511.yml -openapi_spec_hash: a8fbbcaa38e91c7f97313620b42d8d62 +configured_endpoints: 1 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fstagehand-516862e7e90968bc55ac44ce7ffe2d5c1f738c11757471c2b9e0e4cff9384f2c.yml +openapi_spec_hash: 19bbe52a6ae0eec7fc3f6698e831ee55 config_hash: a35b56eb05306a0f02e83c11d57f975f diff --git a/README.md b/README.md index b837b961..443970e4 100644 --- a/README.md +++ b/README.md @@ -41,11 +41,7 @@ client = Stagehand( model_api_key=os.environ.get("MODEL_API_KEY"), # This is the default and can be omitted ) -response = client.sessions.act( - session_id="00000000-your-session-id-000000000000", - input="click the first link on the page", -) -print(response.actions) +response = client.sessions.start() ``` While you can provide a `browserbase_api_key` keyword argument, @@ -74,11 +70,7 @@ client = AsyncStagehand( async def main() -> None: - response = await client.sessions.act( - session_id="00000000-your-session-id-000000000000", - input="click the first link on the page", - ) - print(response.actions) + response = await client.sessions.start() asyncio.run(main()) @@ -117,11 +109,7 @@ async def main() -> None: model_api_key=os.environ.get("MODEL_API_KEY"), # This is the default and can be omitted http_client=DefaultAioHttpClient(), ) as client: - response = await client.sessions.act( - session_id="00000000-your-session-id-000000000000", - input="click the first link on the page", - ) - print(response.actions) + response = await client.sessions.start() asyncio.run(main()) @@ -136,23 +124,6 @@ Nested request parameters are [TypedDicts](https://docs.python.org/3/library/typ Typed requests and responses provide autocomplete and documentation within your editor. If you would like to see type errors in VS Code to help catch bugs earlier, set `python.analysis.typeCheckingMode` to `basic`. -## Nested params - -Nested parameters are dictionaries, typed using `TypedDict`, for example: - -```python -from stagehand import Stagehand - -client = Stagehand() - -response = client.sessions.act( - session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - input="click the sign in button", - options={}, -) -print(response.options) -``` - ## Handling errors When the library is unable to connect to the API (for example, due to network connection problems or a timeout), a subclass of `stagehand.APIConnectionError` is raised. @@ -169,10 +140,7 @@ from stagehand import Stagehand client = Stagehand() try: - client.sessions.start( - browserbase_api_key="your Browserbase API key", - browserbase_project_id="your Browserbase Project ID", - ) + client.sessions.start() except stagehand.APIConnectionError as e: print("The server could not be reached") print(e.__cause__) # an underlying Exception, likely raised within httpx. @@ -215,10 +183,7 @@ client = Stagehand( ) # Or, configure per-request: -client.with_options(max_retries=5).sessions.start( - browserbase_api_key="your Browserbase API key", - browserbase_project_id="your Browserbase Project ID", -) +client.with_options(max_retries=5).sessions.start() ``` ### Timeouts @@ -241,10 +206,7 @@ client = Stagehand( ) # Override per-request: -client.with_options(timeout=5.0).sessions.start( - browserbase_api_key="your Browserbase API key", - browserbase_project_id="your Browserbase Project ID", -) +client.with_options(timeout=5.0).sessions.start() ``` On timeout, an `APITimeoutError` is thrown. @@ -285,14 +247,11 @@ The "raw" Response object can be accessed by prefixing `.with_raw_response.` to from stagehand import Stagehand client = Stagehand() -response = client.sessions.with_raw_response.start( - browserbase_api_key="your Browserbase API key", - browserbase_project_id="your Browserbase Project ID", -) +response = client.sessions.with_raw_response.start() print(response.headers.get('X-My-Header')) session = response.parse() # get the object that `sessions.start()` would have returned -print(session.available) +print(session) ``` These methods return an [`APIResponse`](https://github.com/browserbase/stagehand-python/tree/stainless/src/stagehand/_response.py) object. @@ -306,10 +265,7 @@ The above interface eagerly reads the full response body when you make the reque To stream the response body, use `.with_streaming_response` instead, which requires a context manager and only reads the response body once you call `.read()`, `.text()`, `.json()`, `.iter_bytes()`, `.iter_text()`, `.iter_lines()` or `.parse()`. In the async client, these are async methods. ```python -with client.sessions.with_streaming_response.start( - browserbase_api_key="your Browserbase API key", - browserbase_project_id="your Browserbase Project ID", -) as response: +with client.sessions.with_streaming_response.start() as response: print(response.headers.get("X-My-Header")) for line in response.iter_lines(): diff --git a/api.md b/api.md index 6eb90647..930dcff6 100644 --- a/api.md +++ b/api.md @@ -3,25 +3,9 @@ Types: ```python -from stagehand.types import ( - Action, - ModelConfig, - SessionActResponse, - SessionEndResponse, - SessionExecuteAgentResponse, - SessionExtractResponse, - SessionNavigateResponse, - SessionObserveResponse, - SessionStartResponse, -) +from stagehand.types import Action, ModelConfig ``` Methods: -- client.sessions.act(session_id, \*\*params) -> SessionActResponse -- client.sessions.end(session_id) -> SessionEndResponse -- client.sessions.execute_agent(session_id, \*\*params) -> SessionExecuteAgentResponse -- client.sessions.extract(session_id, \*\*params) -> SessionExtractResponse -- client.sessions.navigate(session_id, \*\*params) -> Optional[SessionNavigateResponse] -- client.sessions.observe(session_id, \*\*params) -> SessionObserveResponse -- client.sessions.start(\*\*params) -> SessionStartResponse +- client.sessions.start(\*\*params) -> object diff --git a/src/stagehand/resources/sessions.py b/src/stagehand/resources/sessions.py index 6b76d2f8..c2db042f 100644 --- a/src/stagehand/resources/sessions.py +++ b/src/stagehand/resources/sessions.py @@ -2,19 +2,9 @@ from __future__ import annotations -from typing import Any, Dict, Optional, cast -from typing_extensions import Literal - import httpx -from ..types import ( - session_act_params, - session_start_params, - session_extract_params, - session_observe_params, - session_navigate_params, - session_execute_agent_params, -) +from ..types import session_start_params from .._types import Body, Omit, Query, Headers, NotGiven, omit, not_given from .._utils import is_given, maybe_transform, strip_not_given, async_maybe_transform from .._compat import cached_property @@ -26,13 +16,6 @@ async_to_streamed_response_wrapper, ) from .._base_client import make_request_options -from ..types.session_act_response import SessionActResponse -from ..types.session_end_response import SessionEndResponse -from ..types.session_start_response import SessionStartResponse -from ..types.session_extract_response import SessionExtractResponse -from ..types.session_observe_response import SessionObserveResponse -from ..types.session_navigate_response import SessionNavigateResponse -from ..types.session_execute_agent_response import SessionExecuteAgentResponse __all__ = ["SessionsResource", "AsyncSessionsResource"] @@ -57,233 +40,27 @@ def with_streaming_response(self) -> SessionsResourceWithStreamingResponse: """ return SessionsResourceWithStreamingResponse(self) - def act( - self, - session_id: str, - *, - input: session_act_params.Input, - frame_id: str | Omit = omit, - options: session_act_params.Options | Omit = omit, - x_stream_response: Literal["true", "false"] | Omit = omit, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> SessionActResponse: - """ - Performs a browser action based on natural language instruction or a specific - action object returned by observe(). - - Args: - input: Natural language instruction - - frame_id: Frame ID to act on (optional) - - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - if not session_id: - raise ValueError(f"Expected a non-empty value for `session_id` but received {session_id!r}") - extra_headers = { - **strip_not_given( - {"x-stream-response": str(x_stream_response) if is_given(x_stream_response) else not_given} - ), - **(extra_headers or {}), - } - return self._post( - f"/sessions/{session_id}/act", - body=maybe_transform( - { - "input": input, - "frame_id": frame_id, - "options": options, - }, - session_act_params.SessionActParams, - ), - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=SessionActResponse, - ) - - def end( - self, - session_id: str, - *, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> SessionEndResponse: - """ - Closes the browser and cleans up all resources associated with the session. - - Args: - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - if not session_id: - raise ValueError(f"Expected a non-empty value for `session_id` but received {session_id!r}") - return self._post( - f"/sessions/{session_id}/end", - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=SessionEndResponse, - ) - - def execute_agent( - self, - session_id: str, - *, - agent_config: session_execute_agent_params.AgentConfig, - execute_options: session_execute_agent_params.ExecuteOptions, - frame_id: str | Omit = omit, - x_stream_response: Literal["true", "false"] | Omit = omit, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> SessionExecuteAgentResponse: - """ - Runs an autonomous agent that can perform multiple actions to complete a complex - task. - - Args: - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - if not session_id: - raise ValueError(f"Expected a non-empty value for `session_id` but received {session_id!r}") - extra_headers = { - **strip_not_given( - {"x-stream-response": str(x_stream_response) if is_given(x_stream_response) else not_given} - ), - **(extra_headers or {}), - } - return self._post( - f"/sessions/{session_id}/agentExecute", - body=maybe_transform( - { - "agent_config": agent_config, - "execute_options": execute_options, - "frame_id": frame_id, - }, - session_execute_agent_params.SessionExecuteAgentParams, - ), - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=SessionExecuteAgentResponse, - ) - - def extract( + def start( self, - session_id: str, *, - frame_id: str | Omit = omit, - instruction: str | Omit = omit, - options: session_extract_params.Options | Omit = omit, - schema: Dict[str, object] | Omit = omit, - x_stream_response: Literal["true", "false"] | Omit = omit, + body: object | Omit = omit, + x_language: object | Omit = omit, + x_sdk_version: object | Omit = omit, + x_sent_at: object | Omit = omit, + x_stream_response: object | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> SessionExtractResponse: - """ - Extracts data from the current page using natural language instructions and - optional JSON schema for structured output. - - Args: - frame_id: Frame ID to extract from - - instruction: Natural language instruction for extraction - - schema: JSON Schema for structured output - - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request + ) -> object: + """Creates a new browser session with the specified configuration. - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - if not session_id: - raise ValueError(f"Expected a non-empty value for `session_id` but received {session_id!r}") - extra_headers = { - **strip_not_given( - {"x-stream-response": str(x_stream_response) if is_given(x_stream_response) else not_given} - ), - **(extra_headers or {}), - } - return cast( - SessionExtractResponse, - self._post( - f"/sessions/{session_id}/extract", - body=maybe_transform( - { - "frame_id": frame_id, - "instruction": instruction, - "options": options, - "schema": schema, - }, - session_extract_params.SessionExtractParams, - ), - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=cast( - Any, SessionExtractResponse - ), # Union types cannot be passed in as arguments in the type system - ), - ) - - def navigate( - self, - session_id: str, - *, - url: str, - frame_id: str | Omit = omit, - options: session_navigate_params.Options | Omit = omit, - x_stream_response: Literal["true", "false"] | Omit = omit, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> Optional[SessionNavigateResponse]: - """ - Navigates the browser to the specified URL and waits for page load. + Returns a + session ID used for all subsequent operations. Args: - url: URL to navigate to - extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -292,149 +69,24 @@ def navigate( timeout: Override the client-level default timeout for this request, in seconds """ - if not session_id: - raise ValueError(f"Expected a non-empty value for `session_id` but received {session_id!r}") extra_headers = { **strip_not_given( - {"x-stream-response": str(x_stream_response) if is_given(x_stream_response) else not_given} - ), - **(extra_headers or {}), - } - return self._post( - f"/sessions/{session_id}/navigate", - body=maybe_transform( { - "url": url, - "frame_id": frame_id, - "options": options, - }, - session_navigate_params.SessionNavigateParams, - ), - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=SessionNavigateResponse, - ) - - def observe( - self, - session_id: str, - *, - frame_id: str | Omit = omit, - instruction: str | Omit = omit, - options: session_observe_params.Options | Omit = omit, - x_stream_response: Literal["true", "false"] | Omit = omit, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> SessionObserveResponse: - """ - Returns a list of candidate actions that can be performed on the page, - optionally filtered by natural language instruction. - - Args: - frame_id: Frame ID to observe - - instruction: Natural language instruction to filter actions - - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - if not session_id: - raise ValueError(f"Expected a non-empty value for `session_id` but received {session_id!r}") - extra_headers = { - **strip_not_given( - {"x-stream-response": str(x_stream_response) if is_given(x_stream_response) else not_given} + "x-language": str(x_language) if is_given(x_language) else not_given, + "x-sdk-version": str(x_sdk_version) if is_given(x_sdk_version) else not_given, + "x-sent-at": str(x_sent_at) if is_given(x_sent_at) else not_given, + "x-stream-response": str(x_stream_response) if is_given(x_stream_response) else not_given, + } ), **(extra_headers or {}), } - return self._post( - f"/sessions/{session_id}/observe", - body=maybe_transform( - { - "frame_id": frame_id, - "instruction": instruction, - "options": options, - }, - session_observe_params.SessionObserveParams, - ), - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=SessionObserveResponse, - ) - - def start( - self, - *, - browserbase_api_key: str, - browserbase_project_id: str, - dom_settle_timeout: int | Omit = omit, - model: str | Omit = omit, - self_heal: bool | Omit = omit, - system_prompt: str | Omit = omit, - verbose: int | Omit = omit, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> SessionStartResponse: - """Initializes a new Stagehand session with a browser instance. - - Returns a session - ID that must be used for all subsequent requests. - - Args: - browserbase_api_key: API key for Browserbase Cloud - - browserbase_project_id: Project ID for Browserbase - - dom_settle_timeout: Timeout in ms to wait for DOM to settle - - model: AI model to use for actions (must be prefixed with provider/) - - self_heal: Enable self-healing for failed actions - - system_prompt: Custom system prompt for AI actions - - verbose: Logging verbosity level - - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ return self._post( "/sessions/start", - body=maybe_transform( - { - "browserbase_api_key": browserbase_api_key, - "browserbase_project_id": browserbase_project_id, - "dom_settle_timeout": dom_settle_timeout, - "model": model, - "self_heal": self_heal, - "system_prompt": system_prompt, - "verbose": verbose, - }, - session_start_params.SessionStartParams, - ), + body=maybe_transform(body, session_start_params.SessionStartParams), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=SessionStartResponse, + cast_to=object, ) @@ -458,233 +110,27 @@ def with_streaming_response(self) -> AsyncSessionsResourceWithStreamingResponse: """ return AsyncSessionsResourceWithStreamingResponse(self) - async def act( - self, - session_id: str, - *, - input: session_act_params.Input, - frame_id: str | Omit = omit, - options: session_act_params.Options | Omit = omit, - x_stream_response: Literal["true", "false"] | Omit = omit, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> SessionActResponse: - """ - Performs a browser action based on natural language instruction or a specific - action object returned by observe(). - - Args: - input: Natural language instruction - - frame_id: Frame ID to act on (optional) - - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - if not session_id: - raise ValueError(f"Expected a non-empty value for `session_id` but received {session_id!r}") - extra_headers = { - **strip_not_given( - {"x-stream-response": str(x_stream_response) if is_given(x_stream_response) else not_given} - ), - **(extra_headers or {}), - } - return await self._post( - f"/sessions/{session_id}/act", - body=await async_maybe_transform( - { - "input": input, - "frame_id": frame_id, - "options": options, - }, - session_act_params.SessionActParams, - ), - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=SessionActResponse, - ) - - async def end( - self, - session_id: str, - *, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> SessionEndResponse: - """ - Closes the browser and cleans up all resources associated with the session. - - Args: - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - if not session_id: - raise ValueError(f"Expected a non-empty value for `session_id` but received {session_id!r}") - return await self._post( - f"/sessions/{session_id}/end", - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=SessionEndResponse, - ) - - async def execute_agent( - self, - session_id: str, - *, - agent_config: session_execute_agent_params.AgentConfig, - execute_options: session_execute_agent_params.ExecuteOptions, - frame_id: str | Omit = omit, - x_stream_response: Literal["true", "false"] | Omit = omit, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> SessionExecuteAgentResponse: - """ - Runs an autonomous agent that can perform multiple actions to complete a complex - task. - - Args: - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - if not session_id: - raise ValueError(f"Expected a non-empty value for `session_id` but received {session_id!r}") - extra_headers = { - **strip_not_given( - {"x-stream-response": str(x_stream_response) if is_given(x_stream_response) else not_given} - ), - **(extra_headers or {}), - } - return await self._post( - f"/sessions/{session_id}/agentExecute", - body=await async_maybe_transform( - { - "agent_config": agent_config, - "execute_options": execute_options, - "frame_id": frame_id, - }, - session_execute_agent_params.SessionExecuteAgentParams, - ), - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=SessionExecuteAgentResponse, - ) - - async def extract( + async def start( self, - session_id: str, *, - frame_id: str | Omit = omit, - instruction: str | Omit = omit, - options: session_extract_params.Options | Omit = omit, - schema: Dict[str, object] | Omit = omit, - x_stream_response: Literal["true", "false"] | Omit = omit, + body: object | Omit = omit, + x_language: object | Omit = omit, + x_sdk_version: object | Omit = omit, + x_sent_at: object | Omit = omit, + x_stream_response: object | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> SessionExtractResponse: - """ - Extracts data from the current page using natural language instructions and - optional JSON schema for structured output. - - Args: - frame_id: Frame ID to extract from - - instruction: Natural language instruction for extraction + ) -> object: + """Creates a new browser session with the specified configuration. - schema: JSON Schema for structured output - - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - if not session_id: - raise ValueError(f"Expected a non-empty value for `session_id` but received {session_id!r}") - extra_headers = { - **strip_not_given( - {"x-stream-response": str(x_stream_response) if is_given(x_stream_response) else not_given} - ), - **(extra_headers or {}), - } - return cast( - SessionExtractResponse, - await self._post( - f"/sessions/{session_id}/extract", - body=await async_maybe_transform( - { - "frame_id": frame_id, - "instruction": instruction, - "options": options, - "schema": schema, - }, - session_extract_params.SessionExtractParams, - ), - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=cast( - Any, SessionExtractResponse - ), # Union types cannot be passed in as arguments in the type system - ), - ) - - async def navigate( - self, - session_id: str, - *, - url: str, - frame_id: str | Omit = omit, - options: session_navigate_params.Options | Omit = omit, - x_stream_response: Literal["true", "false"] | Omit = omit, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> Optional[SessionNavigateResponse]: - """ - Navigates the browser to the specified URL and waits for page load. + Returns a + session ID used for all subsequent operations. Args: - url: URL to navigate to - extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -693,149 +139,24 @@ async def navigate( timeout: Override the client-level default timeout for this request, in seconds """ - if not session_id: - raise ValueError(f"Expected a non-empty value for `session_id` but received {session_id!r}") extra_headers = { **strip_not_given( - {"x-stream-response": str(x_stream_response) if is_given(x_stream_response) else not_given} - ), - **(extra_headers or {}), - } - return await self._post( - f"/sessions/{session_id}/navigate", - body=await async_maybe_transform( { - "url": url, - "frame_id": frame_id, - "options": options, - }, - session_navigate_params.SessionNavigateParams, - ), - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=SessionNavigateResponse, - ) - - async def observe( - self, - session_id: str, - *, - frame_id: str | Omit = omit, - instruction: str | Omit = omit, - options: session_observe_params.Options | Omit = omit, - x_stream_response: Literal["true", "false"] | Omit = omit, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> SessionObserveResponse: - """ - Returns a list of candidate actions that can be performed on the page, - optionally filtered by natural language instruction. - - Args: - frame_id: Frame ID to observe - - instruction: Natural language instruction to filter actions - - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - if not session_id: - raise ValueError(f"Expected a non-empty value for `session_id` but received {session_id!r}") - extra_headers = { - **strip_not_given( - {"x-stream-response": str(x_stream_response) if is_given(x_stream_response) else not_given} + "x-language": str(x_language) if is_given(x_language) else not_given, + "x-sdk-version": str(x_sdk_version) if is_given(x_sdk_version) else not_given, + "x-sent-at": str(x_sent_at) if is_given(x_sent_at) else not_given, + "x-stream-response": str(x_stream_response) if is_given(x_stream_response) else not_given, + } ), **(extra_headers or {}), } - return await self._post( - f"/sessions/{session_id}/observe", - body=await async_maybe_transform( - { - "frame_id": frame_id, - "instruction": instruction, - "options": options, - }, - session_observe_params.SessionObserveParams, - ), - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=SessionObserveResponse, - ) - - async def start( - self, - *, - browserbase_api_key: str, - browserbase_project_id: str, - dom_settle_timeout: int | Omit = omit, - model: str | Omit = omit, - self_heal: bool | Omit = omit, - system_prompt: str | Omit = omit, - verbose: int | Omit = omit, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> SessionStartResponse: - """Initializes a new Stagehand session with a browser instance. - - Returns a session - ID that must be used for all subsequent requests. - - Args: - browserbase_api_key: API key for Browserbase Cloud - - browserbase_project_id: Project ID for Browserbase - - dom_settle_timeout: Timeout in ms to wait for DOM to settle - - model: AI model to use for actions (must be prefixed with provider/) - - self_heal: Enable self-healing for failed actions - - system_prompt: Custom system prompt for AI actions - - verbose: Logging verbosity level - - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ return await self._post( "/sessions/start", - body=await async_maybe_transform( - { - "browserbase_api_key": browserbase_api_key, - "browserbase_project_id": browserbase_project_id, - "dom_settle_timeout": dom_settle_timeout, - "model": model, - "self_heal": self_heal, - "system_prompt": system_prompt, - "verbose": verbose, - }, - session_start_params.SessionStartParams, - ), + body=await async_maybe_transform(body, session_start_params.SessionStartParams), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=SessionStartResponse, + cast_to=object, ) @@ -843,24 +164,6 @@ class SessionsResourceWithRawResponse: def __init__(self, sessions: SessionsResource) -> None: self._sessions = sessions - self.act = to_raw_response_wrapper( - sessions.act, - ) - self.end = to_raw_response_wrapper( - sessions.end, - ) - self.execute_agent = to_raw_response_wrapper( - sessions.execute_agent, - ) - self.extract = to_raw_response_wrapper( - sessions.extract, - ) - self.navigate = to_raw_response_wrapper( - sessions.navigate, - ) - self.observe = to_raw_response_wrapper( - sessions.observe, - ) self.start = to_raw_response_wrapper( sessions.start, ) @@ -870,24 +173,6 @@ class AsyncSessionsResourceWithRawResponse: def __init__(self, sessions: AsyncSessionsResource) -> None: self._sessions = sessions - self.act = async_to_raw_response_wrapper( - sessions.act, - ) - self.end = async_to_raw_response_wrapper( - sessions.end, - ) - self.execute_agent = async_to_raw_response_wrapper( - sessions.execute_agent, - ) - self.extract = async_to_raw_response_wrapper( - sessions.extract, - ) - self.navigate = async_to_raw_response_wrapper( - sessions.navigate, - ) - self.observe = async_to_raw_response_wrapper( - sessions.observe, - ) self.start = async_to_raw_response_wrapper( sessions.start, ) @@ -897,24 +182,6 @@ class SessionsResourceWithStreamingResponse: def __init__(self, sessions: SessionsResource) -> None: self._sessions = sessions - self.act = to_streamed_response_wrapper( - sessions.act, - ) - self.end = to_streamed_response_wrapper( - sessions.end, - ) - self.execute_agent = to_streamed_response_wrapper( - sessions.execute_agent, - ) - self.extract = to_streamed_response_wrapper( - sessions.extract, - ) - self.navigate = to_streamed_response_wrapper( - sessions.navigate, - ) - self.observe = to_streamed_response_wrapper( - sessions.observe, - ) self.start = to_streamed_response_wrapper( sessions.start, ) @@ -924,24 +191,6 @@ class AsyncSessionsResourceWithStreamingResponse: def __init__(self, sessions: AsyncSessionsResource) -> None: self._sessions = sessions - self.act = async_to_streamed_response_wrapper( - sessions.act, - ) - self.end = async_to_streamed_response_wrapper( - sessions.end, - ) - self.execute_agent = async_to_streamed_response_wrapper( - sessions.execute_agent, - ) - self.extract = async_to_streamed_response_wrapper( - sessions.extract, - ) - self.navigate = async_to_streamed_response_wrapper( - sessions.navigate, - ) - self.observe = async_to_streamed_response_wrapper( - sessions.observe, - ) self.start = async_to_streamed_response_wrapper( sessions.start, ) diff --git a/src/stagehand/types/__init__.py b/src/stagehand/types/__init__.py index 077ed5ad..5cd88eeb 100644 --- a/src/stagehand/types/__init__.py +++ b/src/stagehand/types/__init__.py @@ -2,19 +2,4 @@ from __future__ import annotations -from .action import Action as Action -from .action_param import ActionParam as ActionParam -from .model_config_param import ModelConfigParam as ModelConfigParam -from .session_act_params import SessionActParams as SessionActParams -from .session_act_response import SessionActResponse as SessionActResponse -from .session_end_response import SessionEndResponse as SessionEndResponse from .session_start_params import SessionStartParams as SessionStartParams -from .session_extract_params import SessionExtractParams as SessionExtractParams -from .session_observe_params import SessionObserveParams as SessionObserveParams -from .session_start_response import SessionStartResponse as SessionStartResponse -from .session_navigate_params import SessionNavigateParams as SessionNavigateParams -from .session_extract_response import SessionExtractResponse as SessionExtractResponse -from .session_observe_response import SessionObserveResponse as SessionObserveResponse -from .session_navigate_response import SessionNavigateResponse as SessionNavigateResponse -from .session_execute_agent_params import SessionExecuteAgentParams as SessionExecuteAgentParams -from .session_execute_agent_response import SessionExecuteAgentResponse as SessionExecuteAgentResponse diff --git a/src/stagehand/types/action.py b/src/stagehand/types/action.py deleted file mode 100644 index c68f9aaf..00000000 --- a/src/stagehand/types/action.py +++ /dev/null @@ -1,26 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from typing import List, Optional - -from pydantic import Field as FieldInfo - -from .._models import BaseModel - -__all__ = ["Action"] - - -class Action(BaseModel): - arguments: List[str] - """Arguments for the method""" - - description: str - """Human-readable description of the action""" - - method: str - """Method to execute (e.g., "click", "fill")""" - - selector: str - """CSS or XPath selector for the element""" - - backend_node_id: Optional[int] = FieldInfo(alias="backendNodeId", default=None) - """CDP backend node ID""" diff --git a/src/stagehand/types/action_param.py b/src/stagehand/types/action_param.py deleted file mode 100644 index 1a0cc408..00000000 --- a/src/stagehand/types/action_param.py +++ /dev/null @@ -1,27 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -from typing_extensions import Required, Annotated, TypedDict - -from .._types import SequenceNotStr -from .._utils import PropertyInfo - -__all__ = ["ActionParam"] - - -class ActionParam(TypedDict, total=False): - arguments: Required[SequenceNotStr[str]] - """Arguments for the method""" - - description: Required[str] - """Human-readable description of the action""" - - method: Required[str] - """Method to execute (e.g., "click", "fill")""" - - selector: Required[str] - """CSS or XPath selector for the element""" - - backend_node_id: Annotated[int, PropertyInfo(alias="backendNodeId")] - """CDP backend node ID""" diff --git a/src/stagehand/types/model_config_param.py b/src/stagehand/types/model_config_param.py deleted file mode 100644 index 2cd1e97d..00000000 --- a/src/stagehand/types/model_config_param.py +++ /dev/null @@ -1,22 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -from typing_extensions import Literal, Annotated, TypedDict - -from .._utils import PropertyInfo - -__all__ = ["ModelConfigParam"] - - -class ModelConfigParam(TypedDict, total=False): - api_key: Annotated[str, PropertyInfo(alias="apiKey")] - """API key for the model provider""" - - base_url: Annotated[str, PropertyInfo(alias="baseURL")] - """Custom base URL for API""" - - model: str - """Model name""" - - provider: Literal["openai", "anthropic", "google"] diff --git a/src/stagehand/types/session_act_params.py b/src/stagehand/types/session_act_params.py deleted file mode 100644 index 80e60817..00000000 --- a/src/stagehand/types/session_act_params.py +++ /dev/null @@ -1,37 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -from typing import Dict, Union -from typing_extensions import Literal, Required, Annotated, TypeAlias, TypedDict - -from .._utils import PropertyInfo -from .action_param import ActionParam -from .model_config_param import ModelConfigParam - -__all__ = ["SessionActParams", "Input", "Options"] - - -class SessionActParams(TypedDict, total=False): - input: Required[Input] - """Natural language instruction""" - - frame_id: Annotated[str, PropertyInfo(alias="frameId")] - """Frame ID to act on (optional)""" - - options: Options - - x_stream_response: Annotated[Literal["true", "false"], PropertyInfo(alias="x-stream-response")] - - -Input: TypeAlias = Union[str, ActionParam] - - -class Options(TypedDict, total=False): - model: ModelConfigParam - - timeout: int - """Timeout in milliseconds""" - - variables: Dict[str, str] - """Template variables for instruction""" diff --git a/src/stagehand/types/session_act_response.py b/src/stagehand/types/session_act_response.py deleted file mode 100644 index 7a99253f..00000000 --- a/src/stagehand/types/session_act_response.py +++ /dev/null @@ -1,19 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from typing import List - -from .action import Action -from .._models import BaseModel - -__all__ = ["SessionActResponse"] - - -class SessionActResponse(BaseModel): - actions: List[Action] - """Actions that were executed""" - - message: str - """Result message""" - - success: bool - """Whether the action succeeded""" diff --git a/src/stagehand/types/session_end_response.py b/src/stagehand/types/session_end_response.py deleted file mode 100644 index d088e6ee..00000000 --- a/src/stagehand/types/session_end_response.py +++ /dev/null @@ -1,11 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from typing import Optional - -from .._models import BaseModel - -__all__ = ["SessionEndResponse"] - - -class SessionEndResponse(BaseModel): - success: Optional[bool] = None diff --git a/src/stagehand/types/session_execute_agent_params.py b/src/stagehand/types/session_execute_agent_params.py deleted file mode 100644 index c3493805..00000000 --- a/src/stagehand/types/session_execute_agent_params.py +++ /dev/null @@ -1,46 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -from typing import Union -from typing_extensions import Literal, Required, Annotated, TypeAlias, TypedDict - -from .._utils import PropertyInfo -from .model_config_param import ModelConfigParam - -__all__ = ["SessionExecuteAgentParams", "AgentConfig", "AgentConfigModel", "ExecuteOptions"] - - -class SessionExecuteAgentParams(TypedDict, total=False): - agent_config: Required[Annotated[AgentConfig, PropertyInfo(alias="agentConfig")]] - - execute_options: Required[Annotated[ExecuteOptions, PropertyInfo(alias="executeOptions")]] - - frame_id: Annotated[str, PropertyInfo(alias="frameId")] - - x_stream_response: Annotated[Literal["true", "false"], PropertyInfo(alias="x-stream-response")] - - -AgentConfigModel: TypeAlias = Union[str, ModelConfigParam] - - -class AgentConfig(TypedDict, total=False): - cua: bool - """Enable Computer Use Agent mode""" - - model: AgentConfigModel - - provider: Literal["openai", "anthropic", "google"] - - system_prompt: Annotated[str, PropertyInfo(alias="systemPrompt")] - - -class ExecuteOptions(TypedDict, total=False): - instruction: Required[str] - """Task for the agent to complete""" - - highlight_cursor: Annotated[bool, PropertyInfo(alias="highlightCursor")] - """Visually highlight the cursor during actions""" - - max_steps: Annotated[int, PropertyInfo(alias="maxSteps")] - """Maximum number of steps the agent can take""" diff --git a/src/stagehand/types/session_execute_agent_response.py b/src/stagehand/types/session_execute_agent_response.py deleted file mode 100644 index 80d0c0b2..00000000 --- a/src/stagehand/types/session_execute_agent_response.py +++ /dev/null @@ -1,12 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from typing import Optional - -from .._models import BaseModel - -__all__ = ["SessionExecuteAgentResponse"] - - -class SessionExecuteAgentResponse(BaseModel): - message: Optional[str] = None - """Final message from the agent""" diff --git a/src/stagehand/types/session_extract_params.py b/src/stagehand/types/session_extract_params.py deleted file mode 100644 index 05005193..00000000 --- a/src/stagehand/types/session_extract_params.py +++ /dev/null @@ -1,35 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -from typing import Dict -from typing_extensions import Literal, Annotated, TypedDict - -from .._utils import PropertyInfo -from .model_config_param import ModelConfigParam - -__all__ = ["SessionExtractParams", "Options"] - - -class SessionExtractParams(TypedDict, total=False): - frame_id: Annotated[str, PropertyInfo(alias="frameId")] - """Frame ID to extract from""" - - instruction: str - """Natural language instruction for extraction""" - - options: Options - - schema: Dict[str, object] - """JSON Schema for structured output""" - - x_stream_response: Annotated[Literal["true", "false"], PropertyInfo(alias="x-stream-response")] - - -class Options(TypedDict, total=False): - model: ModelConfigParam - - selector: str - """Extract only from elements matching this selector""" - - timeout: int diff --git a/src/stagehand/types/session_extract_response.py b/src/stagehand/types/session_extract_response.py deleted file mode 100644 index 723ed97c..00000000 --- a/src/stagehand/types/session_extract_response.py +++ /dev/null @@ -1,17 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from typing import Dict, Union, Optional -from typing_extensions import TypeAlias - -from .._models import BaseModel - -__all__ = ["SessionExtractResponse", "Extraction"] - - -class Extraction(BaseModel): - """Default extraction result""" - - extraction: Optional[str] = None - - -SessionExtractResponse: TypeAlias = Union[Extraction, Dict[str, object]] diff --git a/src/stagehand/types/session_navigate_params.py b/src/stagehand/types/session_navigate_params.py deleted file mode 100644 index a96eacfc..00000000 --- a/src/stagehand/types/session_navigate_params.py +++ /dev/null @@ -1,25 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -from typing_extensions import Literal, Required, Annotated, TypedDict - -from .._utils import PropertyInfo - -__all__ = ["SessionNavigateParams", "Options"] - - -class SessionNavigateParams(TypedDict, total=False): - url: Required[str] - """URL to navigate to""" - - frame_id: Annotated[str, PropertyInfo(alias="frameId")] - - options: Options - - x_stream_response: Annotated[Literal["true", "false"], PropertyInfo(alias="x-stream-response")] - - -class Options(TypedDict, total=False): - wait_until: Annotated[Literal["load", "domcontentloaded", "networkidle"], PropertyInfo(alias="waitUntil")] - """When to consider navigation complete""" diff --git a/src/stagehand/types/session_navigate_response.py b/src/stagehand/types/session_navigate_response.py deleted file mode 100644 index d7422b9f..00000000 --- a/src/stagehand/types/session_navigate_response.py +++ /dev/null @@ -1,17 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from typing import Optional - -from .._models import BaseModel - -__all__ = ["SessionNavigateResponse"] - - -class SessionNavigateResponse(BaseModel): - """Navigation response (may be null)""" - - ok: Optional[bool] = None - - status: Optional[int] = None - - url: Optional[str] = None diff --git a/src/stagehand/types/session_observe_params.py b/src/stagehand/types/session_observe_params.py deleted file mode 100644 index c6b8a777..00000000 --- a/src/stagehand/types/session_observe_params.py +++ /dev/null @@ -1,31 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -from typing_extensions import Literal, Annotated, TypedDict - -from .._utils import PropertyInfo -from .model_config_param import ModelConfigParam - -__all__ = ["SessionObserveParams", "Options"] - - -class SessionObserveParams(TypedDict, total=False): - frame_id: Annotated[str, PropertyInfo(alias="frameId")] - """Frame ID to observe""" - - instruction: str - """Natural language instruction to filter actions""" - - options: Options - - x_stream_response: Annotated[Literal["true", "false"], PropertyInfo(alias="x-stream-response")] - - -class Options(TypedDict, total=False): - model: ModelConfigParam - - selector: str - """Observe only elements matching this selector""" - - timeout: int diff --git a/src/stagehand/types/session_observe_response.py b/src/stagehand/types/session_observe_response.py deleted file mode 100644 index fd8b2fbe..00000000 --- a/src/stagehand/types/session_observe_response.py +++ /dev/null @@ -1,10 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from typing import List -from typing_extensions import TypeAlias - -from .action import Action - -__all__ = ["SessionObserveResponse"] - -SessionObserveResponse: TypeAlias = List[Action] diff --git a/src/stagehand/types/session_start_params.py b/src/stagehand/types/session_start_params.py index 09320030..790d685e 100644 --- a/src/stagehand/types/session_start_params.py +++ b/src/stagehand/types/session_start_params.py @@ -2,7 +2,7 @@ from __future__ import annotations -from typing_extensions import Required, Annotated, TypedDict +from typing_extensions import Annotated, TypedDict from .._utils import PropertyInfo @@ -10,23 +10,12 @@ class SessionStartParams(TypedDict, total=False): - browserbase_api_key: Required[Annotated[str, PropertyInfo(alias="BROWSERBASE_API_KEY")]] - """API key for Browserbase Cloud""" + body: object - browserbase_project_id: Required[Annotated[str, PropertyInfo(alias="BROWSERBASE_PROJECT_ID")]] - """Project ID for Browserbase""" + x_language: Annotated[object, PropertyInfo(alias="x-language")] - dom_settle_timeout: Annotated[int, PropertyInfo(alias="domSettleTimeout")] - """Timeout in ms to wait for DOM to settle""" + x_sdk_version: Annotated[object, PropertyInfo(alias="x-sdk-version")] - model: str - """AI model to use for actions (must be prefixed with provider/)""" + x_sent_at: Annotated[object, PropertyInfo(alias="x-sent-at")] - self_heal: Annotated[bool, PropertyInfo(alias="selfHeal")] - """Enable self-healing for failed actions""" - - system_prompt: Annotated[str, PropertyInfo(alias="systemPrompt")] - """Custom system prompt for AI actions""" - - verbose: int - """Logging verbosity level""" + x_stream_response: Annotated[object, PropertyInfo(alias="x-stream-response")] diff --git a/src/stagehand/types/session_start_response.py b/src/stagehand/types/session_start_response.py deleted file mode 100644 index b5862c8b..00000000 --- a/src/stagehand/types/session_start_response.py +++ /dev/null @@ -1,15 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from pydantic import Field as FieldInfo - -from .._models import BaseModel - -__all__ = ["SessionStartResponse"] - - -class SessionStartResponse(BaseModel): - available: bool - """Whether the session is ready to use""" - - session_id: str = FieldInfo(alias="sessionId") - """Unique identifier for the session""" diff --git a/tests/api_resources/test_sessions.py b/tests/api_resources/test_sessions.py index f38e02d6..1e0fe2e0 100644 --- a/tests/api_resources/test_sessions.py +++ b/tests/api_resources/test_sessions.py @@ -3,21 +3,12 @@ from __future__ import annotations import os -from typing import Any, Optional, cast +from typing import Any, cast import pytest from stagehand import Stagehand, AsyncStagehand from tests.utils import assert_matches_type -from stagehand.types import ( - SessionActResponse, - SessionEndResponse, - SessionStartResponse, - SessionExtractResponse, - SessionObserveResponse, - SessionNavigateResponse, - SessionExecuteAgentResponse, -) base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") @@ -25,419 +16,43 @@ class TestSessions: parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_method_act(self, client: Stagehand) -> None: - session = client.sessions.act( - session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - input="click the sign in button", - ) - assert_matches_type(SessionActResponse, session, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_method_act_with_all_params(self, client: Stagehand) -> None: - session = client.sessions.act( - session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - input="click the sign in button", - frame_id="frameId", - options={ - "model": { - "api_key": "apiKey", - "base_url": "https://example.com", - "model": "model", - "provider": "openai", - }, - "timeout": 0, - "variables": {"foo": "string"}, - }, - x_stream_response="true", - ) - assert_matches_type(SessionActResponse, session, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_raw_response_act(self, client: Stagehand) -> None: - response = client.sessions.with_raw_response.act( - session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - input="click the sign in button", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - session = response.parse() - assert_matches_type(SessionActResponse, session, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_streaming_response_act(self, client: Stagehand) -> None: - with client.sessions.with_streaming_response.act( - session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - input="click the sign in button", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - session = response.parse() - assert_matches_type(SessionActResponse, session, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_path_params_act(self, client: Stagehand) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `session_id` but received ''"): - client.sessions.with_raw_response.act( - session_id="", - input="click the sign in button", - ) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_method_end(self, client: Stagehand) -> None: - session = client.sessions.end( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) - assert_matches_type(SessionEndResponse, session, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_raw_response_end(self, client: Stagehand) -> None: - response = client.sessions.with_raw_response.end( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - session = response.parse() - assert_matches_type(SessionEndResponse, session, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_streaming_response_end(self, client: Stagehand) -> None: - with client.sessions.with_streaming_response.end( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - session = response.parse() - assert_matches_type(SessionEndResponse, session, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_path_params_end(self, client: Stagehand) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `session_id` but received ''"): - client.sessions.with_raw_response.end( - "", - ) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_method_execute_agent(self, client: Stagehand) -> None: - session = client.sessions.execute_agent( - session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - agent_config={}, - execute_options={"instruction": "Find and click the first product"}, - ) - assert_matches_type(SessionExecuteAgentResponse, session, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_method_execute_agent_with_all_params(self, client: Stagehand) -> None: - session = client.sessions.execute_agent( - session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - agent_config={ - "cua": True, - "model": "openai/gpt-4o", - "provider": "openai", - "system_prompt": "systemPrompt", - }, - execute_options={ - "instruction": "Find and click the first product", - "highlight_cursor": True, - "max_steps": 10, - }, - frame_id="frameId", - x_stream_response="true", - ) - assert_matches_type(SessionExecuteAgentResponse, session, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_raw_response_execute_agent(self, client: Stagehand) -> None: - response = client.sessions.with_raw_response.execute_agent( - session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - agent_config={}, - execute_options={"instruction": "Find and click the first product"}, - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - session = response.parse() - assert_matches_type(SessionExecuteAgentResponse, session, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_streaming_response_execute_agent(self, client: Stagehand) -> None: - with client.sessions.with_streaming_response.execute_agent( - session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - agent_config={}, - execute_options={"instruction": "Find and click the first product"}, - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - session = response.parse() - assert_matches_type(SessionExecuteAgentResponse, session, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_path_params_execute_agent(self, client: Stagehand) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `session_id` but received ''"): - client.sessions.with_raw_response.execute_agent( - session_id="", - agent_config={}, - execute_options={"instruction": "Find and click the first product"}, - ) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_method_extract(self, client: Stagehand) -> None: - session = client.sessions.extract( - session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) - assert_matches_type(SessionExtractResponse, session, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_method_extract_with_all_params(self, client: Stagehand) -> None: - session = client.sessions.extract( - session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - frame_id="frameId", - instruction="extract the page title", - options={ - "model": { - "api_key": "apiKey", - "base_url": "https://example.com", - "model": "model", - "provider": "openai", - }, - "selector": "selector", - "timeout": 0, - }, - schema={"foo": "bar"}, - x_stream_response="true", - ) - assert_matches_type(SessionExtractResponse, session, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_raw_response_extract(self, client: Stagehand) -> None: - response = client.sessions.with_raw_response.extract( - session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - session = response.parse() - assert_matches_type(SessionExtractResponse, session, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_streaming_response_extract(self, client: Stagehand) -> None: - with client.sessions.with_streaming_response.extract( - session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - session = response.parse() - assert_matches_type(SessionExtractResponse, session, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_path_params_extract(self, client: Stagehand) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `session_id` but received ''"): - client.sessions.with_raw_response.extract( - session_id="", - ) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_method_navigate(self, client: Stagehand) -> None: - session = client.sessions.navigate( - session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - url="https://example.com", - ) - assert_matches_type(Optional[SessionNavigateResponse], session, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_method_navigate_with_all_params(self, client: Stagehand) -> None: - session = client.sessions.navigate( - session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - url="https://example.com", - frame_id="frameId", - options={"wait_until": "load"}, - x_stream_response="true", - ) - assert_matches_type(Optional[SessionNavigateResponse], session, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_raw_response_navigate(self, client: Stagehand) -> None: - response = client.sessions.with_raw_response.navigate( - session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - url="https://example.com", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - session = response.parse() - assert_matches_type(Optional[SessionNavigateResponse], session, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_streaming_response_navigate(self, client: Stagehand) -> None: - with client.sessions.with_streaming_response.navigate( - session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - url="https://example.com", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - session = response.parse() - assert_matches_type(Optional[SessionNavigateResponse], session, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_path_params_navigate(self, client: Stagehand) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `session_id` but received ''"): - client.sessions.with_raw_response.navigate( - session_id="", - url="https://example.com", - ) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_method_observe(self, client: Stagehand) -> None: - session = client.sessions.observe( - session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) - assert_matches_type(SessionObserveResponse, session, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_method_observe_with_all_params(self, client: Stagehand) -> None: - session = client.sessions.observe( - session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - frame_id="frameId", - instruction="instruction", - options={ - "model": { - "api_key": "apiKey", - "base_url": "https://example.com", - "model": "model", - "provider": "openai", - }, - "selector": "selector", - "timeout": 0, - }, - x_stream_response="true", - ) - assert_matches_type(SessionObserveResponse, session, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_raw_response_observe(self, client: Stagehand) -> None: - response = client.sessions.with_raw_response.observe( - session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - session = response.parse() - assert_matches_type(SessionObserveResponse, session, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_streaming_response_observe(self, client: Stagehand) -> None: - with client.sessions.with_streaming_response.observe( - session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - session = response.parse() - assert_matches_type(SessionObserveResponse, session, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_path_params_observe(self, client: Stagehand) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `session_id` but received ''"): - client.sessions.with_raw_response.observe( - session_id="", - ) - @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_method_start(self, client: Stagehand) -> None: - session = client.sessions.start( - browserbase_api_key="BROWSERBASE_API_KEY", - browserbase_project_id="BROWSERBASE_PROJECT_ID", - ) - assert_matches_type(SessionStartResponse, session, path=["response"]) + session = client.sessions.start() + assert_matches_type(object, session, path=["response"]) @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_method_start_with_all_params(self, client: Stagehand) -> None: session = client.sessions.start( - browserbase_api_key="BROWSERBASE_API_KEY", - browserbase_project_id="BROWSERBASE_PROJECT_ID", - dom_settle_timeout=0, - model="openai/gpt-4o", - self_heal=True, - system_prompt="systemPrompt", - verbose=1, + body={}, + x_language={}, + x_sdk_version={}, + x_sent_at={}, + x_stream_response={}, ) - assert_matches_type(SessionStartResponse, session, path=["response"]) + assert_matches_type(object, session, path=["response"]) @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_raw_response_start(self, client: Stagehand) -> None: - response = client.sessions.with_raw_response.start( - browserbase_api_key="BROWSERBASE_API_KEY", - browserbase_project_id="BROWSERBASE_PROJECT_ID", - ) + response = client.sessions.with_raw_response.start() assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" session = response.parse() - assert_matches_type(SessionStartResponse, session, path=["response"]) + assert_matches_type(object, session, path=["response"]) @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_streaming_response_start(self, client: Stagehand) -> None: - with client.sessions.with_streaming_response.start( - browserbase_api_key="BROWSERBASE_API_KEY", - browserbase_project_id="BROWSERBASE_PROJECT_ID", - ) as response: + with client.sessions.with_streaming_response.start() as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" session = response.parse() - assert_matches_type(SessionStartResponse, session, path=["response"]) + assert_matches_type(object, session, path=["response"]) assert cast(Any, response.is_closed) is True @@ -447,418 +62,42 @@ class TestAsyncSessions: "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] ) - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_method_act(self, async_client: AsyncStagehand) -> None: - session = await async_client.sessions.act( - session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - input="click the sign in button", - ) - assert_matches_type(SessionActResponse, session, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_method_act_with_all_params(self, async_client: AsyncStagehand) -> None: - session = await async_client.sessions.act( - session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - input="click the sign in button", - frame_id="frameId", - options={ - "model": { - "api_key": "apiKey", - "base_url": "https://example.com", - "model": "model", - "provider": "openai", - }, - "timeout": 0, - "variables": {"foo": "string"}, - }, - x_stream_response="true", - ) - assert_matches_type(SessionActResponse, session, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_raw_response_act(self, async_client: AsyncStagehand) -> None: - response = await async_client.sessions.with_raw_response.act( - session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - input="click the sign in button", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - session = await response.parse() - assert_matches_type(SessionActResponse, session, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_streaming_response_act(self, async_client: AsyncStagehand) -> None: - async with async_client.sessions.with_streaming_response.act( - session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - input="click the sign in button", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - session = await response.parse() - assert_matches_type(SessionActResponse, session, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_path_params_act(self, async_client: AsyncStagehand) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `session_id` but received ''"): - await async_client.sessions.with_raw_response.act( - session_id="", - input="click the sign in button", - ) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_method_end(self, async_client: AsyncStagehand) -> None: - session = await async_client.sessions.end( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) - assert_matches_type(SessionEndResponse, session, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_raw_response_end(self, async_client: AsyncStagehand) -> None: - response = await async_client.sessions.with_raw_response.end( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - session = await response.parse() - assert_matches_type(SessionEndResponse, session, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_streaming_response_end(self, async_client: AsyncStagehand) -> None: - async with async_client.sessions.with_streaming_response.end( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - session = await response.parse() - assert_matches_type(SessionEndResponse, session, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_path_params_end(self, async_client: AsyncStagehand) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `session_id` but received ''"): - await async_client.sessions.with_raw_response.end( - "", - ) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_method_execute_agent(self, async_client: AsyncStagehand) -> None: - session = await async_client.sessions.execute_agent( - session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - agent_config={}, - execute_options={"instruction": "Find and click the first product"}, - ) - assert_matches_type(SessionExecuteAgentResponse, session, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_method_execute_agent_with_all_params(self, async_client: AsyncStagehand) -> None: - session = await async_client.sessions.execute_agent( - session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - agent_config={ - "cua": True, - "model": "openai/gpt-4o", - "provider": "openai", - "system_prompt": "systemPrompt", - }, - execute_options={ - "instruction": "Find and click the first product", - "highlight_cursor": True, - "max_steps": 10, - }, - frame_id="frameId", - x_stream_response="true", - ) - assert_matches_type(SessionExecuteAgentResponse, session, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_raw_response_execute_agent(self, async_client: AsyncStagehand) -> None: - response = await async_client.sessions.with_raw_response.execute_agent( - session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - agent_config={}, - execute_options={"instruction": "Find and click the first product"}, - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - session = await response.parse() - assert_matches_type(SessionExecuteAgentResponse, session, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_streaming_response_execute_agent(self, async_client: AsyncStagehand) -> None: - async with async_client.sessions.with_streaming_response.execute_agent( - session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - agent_config={}, - execute_options={"instruction": "Find and click the first product"}, - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - session = await response.parse() - assert_matches_type(SessionExecuteAgentResponse, session, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_path_params_execute_agent(self, async_client: AsyncStagehand) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `session_id` but received ''"): - await async_client.sessions.with_raw_response.execute_agent( - session_id="", - agent_config={}, - execute_options={"instruction": "Find and click the first product"}, - ) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_method_extract(self, async_client: AsyncStagehand) -> None: - session = await async_client.sessions.extract( - session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) - assert_matches_type(SessionExtractResponse, session, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_method_extract_with_all_params(self, async_client: AsyncStagehand) -> None: - session = await async_client.sessions.extract( - session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - frame_id="frameId", - instruction="extract the page title", - options={ - "model": { - "api_key": "apiKey", - "base_url": "https://example.com", - "model": "model", - "provider": "openai", - }, - "selector": "selector", - "timeout": 0, - }, - schema={"foo": "bar"}, - x_stream_response="true", - ) - assert_matches_type(SessionExtractResponse, session, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_raw_response_extract(self, async_client: AsyncStagehand) -> None: - response = await async_client.sessions.with_raw_response.extract( - session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - session = await response.parse() - assert_matches_type(SessionExtractResponse, session, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_streaming_response_extract(self, async_client: AsyncStagehand) -> None: - async with async_client.sessions.with_streaming_response.extract( - session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - session = await response.parse() - assert_matches_type(SessionExtractResponse, session, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_path_params_extract(self, async_client: AsyncStagehand) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `session_id` but received ''"): - await async_client.sessions.with_raw_response.extract( - session_id="", - ) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_method_navigate(self, async_client: AsyncStagehand) -> None: - session = await async_client.sessions.navigate( - session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - url="https://example.com", - ) - assert_matches_type(Optional[SessionNavigateResponse], session, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_method_navigate_with_all_params(self, async_client: AsyncStagehand) -> None: - session = await async_client.sessions.navigate( - session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - url="https://example.com", - frame_id="frameId", - options={"wait_until": "load"}, - x_stream_response="true", - ) - assert_matches_type(Optional[SessionNavigateResponse], session, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_raw_response_navigate(self, async_client: AsyncStagehand) -> None: - response = await async_client.sessions.with_raw_response.navigate( - session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - url="https://example.com", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - session = await response.parse() - assert_matches_type(Optional[SessionNavigateResponse], session, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_streaming_response_navigate(self, async_client: AsyncStagehand) -> None: - async with async_client.sessions.with_streaming_response.navigate( - session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - url="https://example.com", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - session = await response.parse() - assert_matches_type(Optional[SessionNavigateResponse], session, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_path_params_navigate(self, async_client: AsyncStagehand) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `session_id` but received ''"): - await async_client.sessions.with_raw_response.navigate( - session_id="", - url="https://example.com", - ) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_method_observe(self, async_client: AsyncStagehand) -> None: - session = await async_client.sessions.observe( - session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) - assert_matches_type(SessionObserveResponse, session, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_method_observe_with_all_params(self, async_client: AsyncStagehand) -> None: - session = await async_client.sessions.observe( - session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - frame_id="frameId", - instruction="instruction", - options={ - "model": { - "api_key": "apiKey", - "base_url": "https://example.com", - "model": "model", - "provider": "openai", - }, - "selector": "selector", - "timeout": 0, - }, - x_stream_response="true", - ) - assert_matches_type(SessionObserveResponse, session, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_raw_response_observe(self, async_client: AsyncStagehand) -> None: - response = await async_client.sessions.with_raw_response.observe( - session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - session = await response.parse() - assert_matches_type(SessionObserveResponse, session, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_streaming_response_observe(self, async_client: AsyncStagehand) -> None: - async with async_client.sessions.with_streaming_response.observe( - session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - session = await response.parse() - assert_matches_type(SessionObserveResponse, session, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_path_params_observe(self, async_client: AsyncStagehand) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `session_id` but received ''"): - await async_client.sessions.with_raw_response.observe( - session_id="", - ) - @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_method_start(self, async_client: AsyncStagehand) -> None: - session = await async_client.sessions.start( - browserbase_api_key="BROWSERBASE_API_KEY", - browserbase_project_id="BROWSERBASE_PROJECT_ID", - ) - assert_matches_type(SessionStartResponse, session, path=["response"]) + session = await async_client.sessions.start() + assert_matches_type(object, session, path=["response"]) @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_method_start_with_all_params(self, async_client: AsyncStagehand) -> None: session = await async_client.sessions.start( - browserbase_api_key="BROWSERBASE_API_KEY", - browserbase_project_id="BROWSERBASE_PROJECT_ID", - dom_settle_timeout=0, - model="openai/gpt-4o", - self_heal=True, - system_prompt="systemPrompt", - verbose=1, + body={}, + x_language={}, + x_sdk_version={}, + x_sent_at={}, + x_stream_response={}, ) - assert_matches_type(SessionStartResponse, session, path=["response"]) + assert_matches_type(object, session, path=["response"]) @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_raw_response_start(self, async_client: AsyncStagehand) -> None: - response = await async_client.sessions.with_raw_response.start( - browserbase_api_key="BROWSERBASE_API_KEY", - browserbase_project_id="BROWSERBASE_PROJECT_ID", - ) + response = await async_client.sessions.with_raw_response.start() assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" session = await response.parse() - assert_matches_type(SessionStartResponse, session, path=["response"]) + assert_matches_type(object, session, path=["response"]) @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_streaming_response_start(self, async_client: AsyncStagehand) -> None: - async with async_client.sessions.with_streaming_response.start( - browserbase_api_key="BROWSERBASE_API_KEY", - browserbase_project_id="BROWSERBASE_PROJECT_ID", - ) as response: + async with async_client.sessions.with_streaming_response.start() as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" session = await response.parse() - assert_matches_type(SessionStartResponse, session, path=["response"]) + assert_matches_type(object, session, path=["response"]) assert cast(Any, response.is_closed) is True diff --git a/tests/test_client.py b/tests/test_client.py index 7f28654f..b1f716b0 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -879,9 +879,7 @@ def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter, clien respx_mock.post("/sessions/start").mock(side_effect=httpx.TimeoutException("Test timeout error")) with pytest.raises(APITimeoutError): - client.sessions.with_streaming_response.start( - browserbase_api_key="BROWSERBASE_API_KEY", browserbase_project_id="BROWSERBASE_PROJECT_ID" - ).__enter__() + client.sessions.with_streaming_response.start().__enter__() assert _get_open_connections(client) == 0 @@ -891,9 +889,7 @@ def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter, client respx_mock.post("/sessions/start").mock(return_value=httpx.Response(500)) with pytest.raises(APIStatusError): - client.sessions.with_streaming_response.start( - browserbase_api_key="BROWSERBASE_API_KEY", browserbase_project_id="BROWSERBASE_PROJECT_ID" - ).__enter__() + client.sessions.with_streaming_response.start().__enter__() assert _get_open_connections(client) == 0 @pytest.mark.parametrize("failures_before_success", [0, 2, 4]) @@ -922,9 +918,7 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: respx_mock.post("/sessions/start").mock(side_effect=retry_handler) - response = client.sessions.with_raw_response.start( - browserbase_api_key="BROWSERBASE_API_KEY", browserbase_project_id="BROWSERBASE_PROJECT_ID" - ) + response = client.sessions.with_raw_response.start() assert response.retries_taken == failures_before_success assert int(response.http_request.headers.get("x-stainless-retry-count")) == failures_before_success @@ -948,11 +942,7 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: respx_mock.post("/sessions/start").mock(side_effect=retry_handler) - response = client.sessions.with_raw_response.start( - browserbase_api_key="BROWSERBASE_API_KEY", - browserbase_project_id="BROWSERBASE_PROJECT_ID", - extra_headers={"x-stainless-retry-count": Omit()}, - ) + response = client.sessions.with_raw_response.start(extra_headers={"x-stainless-retry-count": Omit()}) assert len(response.http_request.headers.get_list("x-stainless-retry-count")) == 0 @@ -975,11 +965,7 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: respx_mock.post("/sessions/start").mock(side_effect=retry_handler) - response = client.sessions.with_raw_response.start( - browserbase_api_key="BROWSERBASE_API_KEY", - browserbase_project_id="BROWSERBASE_PROJECT_ID", - extra_headers={"x-stainless-retry-count": "42"}, - ) + response = client.sessions.with_raw_response.start(extra_headers={"x-stainless-retry-count": "42"}) assert response.http_request.headers.get("x-stainless-retry-count") == "42" @@ -1857,9 +1843,7 @@ async def test_retrying_timeout_errors_doesnt_leak( respx_mock.post("/sessions/start").mock(side_effect=httpx.TimeoutException("Test timeout error")) with pytest.raises(APITimeoutError): - await async_client.sessions.with_streaming_response.start( - browserbase_api_key="BROWSERBASE_API_KEY", browserbase_project_id="BROWSERBASE_PROJECT_ID" - ).__aenter__() + await async_client.sessions.with_streaming_response.start().__aenter__() assert _get_open_connections(async_client) == 0 @@ -1871,9 +1855,7 @@ async def test_retrying_status_errors_doesnt_leak( respx_mock.post("/sessions/start").mock(return_value=httpx.Response(500)) with pytest.raises(APIStatusError): - await async_client.sessions.with_streaming_response.start( - browserbase_api_key="BROWSERBASE_API_KEY", browserbase_project_id="BROWSERBASE_PROJECT_ID" - ).__aenter__() + await async_client.sessions.with_streaming_response.start().__aenter__() assert _get_open_connections(async_client) == 0 @pytest.mark.parametrize("failures_before_success", [0, 2, 4]) @@ -1902,9 +1884,7 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: respx_mock.post("/sessions/start").mock(side_effect=retry_handler) - response = await client.sessions.with_raw_response.start( - browserbase_api_key="BROWSERBASE_API_KEY", browserbase_project_id="BROWSERBASE_PROJECT_ID" - ) + response = await client.sessions.with_raw_response.start() assert response.retries_taken == failures_before_success assert int(response.http_request.headers.get("x-stainless-retry-count")) == failures_before_success @@ -1928,11 +1908,7 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: respx_mock.post("/sessions/start").mock(side_effect=retry_handler) - response = await client.sessions.with_raw_response.start( - browserbase_api_key="BROWSERBASE_API_KEY", - browserbase_project_id="BROWSERBASE_PROJECT_ID", - extra_headers={"x-stainless-retry-count": Omit()}, - ) + response = await client.sessions.with_raw_response.start(extra_headers={"x-stainless-retry-count": Omit()}) assert len(response.http_request.headers.get_list("x-stainless-retry-count")) == 0 @@ -1955,11 +1931,7 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: respx_mock.post("/sessions/start").mock(side_effect=retry_handler) - response = await client.sessions.with_raw_response.start( - browserbase_api_key="BROWSERBASE_API_KEY", - browserbase_project_id="BROWSERBASE_PROJECT_ID", - extra_headers={"x-stainless-retry-count": "42"}, - ) + response = await client.sessions.with_raw_response.start(extra_headers={"x-stainless-retry-count": "42"}) assert response.http_request.headers.get("x-stainless-retry-count") == "42" From 34bea31047ddc1c3331f5f6d698912536fb94a40 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 17 Dec 2025 02:15:24 +0000 Subject: [PATCH 28/88] codegen metadata --- .stats.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index f5e93e0f..855c5aaf 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 1 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fstagehand-516862e7e90968bc55ac44ce7ffe2d5c1f738c11757471c2b9e0e4cff9384f2c.yml -openapi_spec_hash: 19bbe52a6ae0eec7fc3f6698e831ee55 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fstagehand-24dc965121b5ad70c846fd8b96ce4638d13bac7e86ba0f50a8d13a5664ac4929.yml +openapi_spec_hash: 5a8e70d2a5358bfd6917f907b4a4d1f3 config_hash: a35b56eb05306a0f02e83c11d57f975f From 030f95bfe6f59332fe7184d4951ba49194b20d5b Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 17 Dec 2025 02:40:32 +0000 Subject: [PATCH 29/88] feat(api): manual updates --- .stats.yml | 8 +- api.md | 6 + src/stagehand/resources/sessions.py | 701 +++++++++++++++++- src/stagehand/types/__init__.py | 5 + src/stagehand/types/session_act_params.py | 21 + .../types/session_execute_agent_params.py | 21 + src/stagehand/types/session_extract_params.py | 21 + .../types/session_navigate_params.py | 21 + src/stagehand/types/session_observe_params.py | 21 + tests/api_resources/test_sessions.py | 562 ++++++++++++++ 10 files changed, 1360 insertions(+), 27 deletions(-) create mode 100644 src/stagehand/types/session_act_params.py create mode 100644 src/stagehand/types/session_execute_agent_params.py create mode 100644 src/stagehand/types/session_extract_params.py create mode 100644 src/stagehand/types/session_navigate_params.py create mode 100644 src/stagehand/types/session_observe_params.py diff --git a/.stats.yml b/.stats.yml index 855c5aaf..c436edec 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ -configured_endpoints: 1 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fstagehand-24dc965121b5ad70c846fd8b96ce4638d13bac7e86ba0f50a8d13a5664ac4929.yml -openapi_spec_hash: 5a8e70d2a5358bfd6917f907b4a4d1f3 -config_hash: a35b56eb05306a0f02e83c11d57f975f +configured_endpoints: 7 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fstagehand-c1a6f03afe5d6823c198e5ac476fb688dacc783dae1fefdf6bf142084e298e16.yml +openapi_spec_hash: d20e8f697ce8d5bb80295fc1e8ce02e8 +config_hash: e457d704d820df5d25acfd379169f132 diff --git a/api.md b/api.md index 930dcff6..f09809b4 100644 --- a/api.md +++ b/api.md @@ -8,4 +8,10 @@ from stagehand.types import Action, ModelConfig Methods: +- client.sessions.act(id, \*\*params) -> object +- client.sessions.end(id) -> object +- client.sessions.execute_agent(id, \*\*params) -> object +- client.sessions.extract(id, \*\*params) -> object +- client.sessions.navigate(id, \*\*params) -> object +- client.sessions.observe(id, \*\*params) -> object - client.sessions.start(\*\*params) -> object diff --git a/src/stagehand/resources/sessions.py b/src/stagehand/resources/sessions.py index c2db042f..c98fbc23 100644 --- a/src/stagehand/resources/sessions.py +++ b/src/stagehand/resources/sessions.py @@ -4,7 +4,14 @@ import httpx -from ..types import session_start_params +from ..types import ( + session_act_params, + session_start_params, + session_extract_params, + session_observe_params, + session_navigate_params, + session_execute_agent_params, +) from .._types import Body, Omit, Query, Headers, NotGiven, omit, not_given from .._utils import is_given, maybe_transform, strip_not_given, async_maybe_transform from .._compat import cached_property @@ -29,19 +36,378 @@ def with_raw_response(self) -> SessionsResourceWithRawResponse: For more information, see https://www.github.com/browserbase/stagehand-python#accessing-raw-response-data-eg-headers """ - return SessionsResourceWithRawResponse(self) + return SessionsResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> SessionsResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/browserbase/stagehand-python#with_streaming_response + """ + return SessionsResourceWithStreamingResponse(self) + + def act( + self, + id: object, + *, + body: object | Omit = omit, + x_language: object | Omit = omit, + x_sdk_version: object | Omit = omit, + x_sent_at: object | Omit = omit, + x_stream_response: object | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> object: + """ + Executes a browser action using natural language instructions or a predefined + Action object. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + extra_headers = { + **strip_not_given( + { + "x-language": str(x_language) if is_given(x_language) else not_given, + "x-sdk-version": str(x_sdk_version) if is_given(x_sdk_version) else not_given, + "x-sent-at": str(x_sent_at) if is_given(x_sent_at) else not_given, + "x-stream-response": str(x_stream_response) if is_given(x_stream_response) else not_given, + } + ), + **(extra_headers or {}), + } + return self._post( + f"/sessions/{id}/act", + body=maybe_transform(body, session_act_params.SessionActParams), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=object, + ) + + def end( + self, + id: object, + *, + x_language: object | Omit = omit, + x_sdk_version: object | Omit = omit, + x_sent_at: object | Omit = omit, + x_stream_response: object | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> object: + """ + Terminates the browser session and releases all associated resources. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + extra_headers = { + **strip_not_given( + { + "x-language": str(x_language) if is_given(x_language) else not_given, + "x-sdk-version": str(x_sdk_version) if is_given(x_sdk_version) else not_given, + "x-sent-at": str(x_sent_at) if is_given(x_sent_at) else not_given, + "x-stream-response": str(x_stream_response) if is_given(x_stream_response) else not_given, + } + ), + **(extra_headers or {}), + } + return self._post( + f"/sessions/{id}/end", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=object, + ) + + def execute_agent( + self, + id: object, + *, + body: object | Omit = omit, + x_language: object | Omit = omit, + x_sdk_version: object | Omit = omit, + x_sent_at: object | Omit = omit, + x_stream_response: object | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> object: + """ + Runs an autonomous AI agent that can perform complex multi-step browser tasks. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + extra_headers = { + **strip_not_given( + { + "x-language": str(x_language) if is_given(x_language) else not_given, + "x-sdk-version": str(x_sdk_version) if is_given(x_sdk_version) else not_given, + "x-sent-at": str(x_sent_at) if is_given(x_sent_at) else not_given, + "x-stream-response": str(x_stream_response) if is_given(x_stream_response) else not_given, + } + ), + **(extra_headers or {}), + } + return self._post( + f"/sessions/{id}/agentExecute", + body=maybe_transform(body, session_execute_agent_params.SessionExecuteAgentParams), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=object, + ) + + def extract( + self, + id: object, + *, + body: object | Omit = omit, + x_language: object | Omit = omit, + x_sdk_version: object | Omit = omit, + x_sent_at: object | Omit = omit, + x_stream_response: object | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> object: + """ + Extracts structured data from the current page using AI-powered analysis. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + extra_headers = { + **strip_not_given( + { + "x-language": str(x_language) if is_given(x_language) else not_given, + "x-sdk-version": str(x_sdk_version) if is_given(x_sdk_version) else not_given, + "x-sent-at": str(x_sent_at) if is_given(x_sent_at) else not_given, + "x-stream-response": str(x_stream_response) if is_given(x_stream_response) else not_given, + } + ), + **(extra_headers or {}), + } + return self._post( + f"/sessions/{id}/extract", + body=maybe_transform(body, session_extract_params.SessionExtractParams), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=object, + ) + + def navigate( + self, + id: object, + *, + body: object | Omit = omit, + x_language: object | Omit = omit, + x_sdk_version: object | Omit = omit, + x_sent_at: object | Omit = omit, + x_stream_response: object | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> object: + """ + Navigates the browser to the specified URL. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + extra_headers = { + **strip_not_given( + { + "x-language": str(x_language) if is_given(x_language) else not_given, + "x-sdk-version": str(x_sdk_version) if is_given(x_sdk_version) else not_given, + "x-sent-at": str(x_sent_at) if is_given(x_sent_at) else not_given, + "x-stream-response": str(x_stream_response) if is_given(x_stream_response) else not_given, + } + ), + **(extra_headers or {}), + } + return self._post( + f"/sessions/{id}/navigate", + body=maybe_transform(body, session_navigate_params.SessionNavigateParams), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=object, + ) + + def observe( + self, + id: object, + *, + body: object | Omit = omit, + x_language: object | Omit = omit, + x_sdk_version: object | Omit = omit, + x_sent_at: object | Omit = omit, + x_stream_response: object | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> object: + """ + Identifies and returns available actions on the current page that match the + given instruction. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + extra_headers = { + **strip_not_given( + { + "x-language": str(x_language) if is_given(x_language) else not_given, + "x-sdk-version": str(x_sdk_version) if is_given(x_sdk_version) else not_given, + "x-sent-at": str(x_sent_at) if is_given(x_sent_at) else not_given, + "x-stream-response": str(x_stream_response) if is_given(x_stream_response) else not_given, + } + ), + **(extra_headers or {}), + } + return self._post( + f"/sessions/{id}/observe", + body=maybe_transform(body, session_observe_params.SessionObserveParams), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=object, + ) + + def start( + self, + *, + body: object | Omit = omit, + x_language: object | Omit = omit, + x_sdk_version: object | Omit = omit, + x_sent_at: object | Omit = omit, + x_stream_response: object | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> object: + """Creates a new browser session with the specified configuration. + + Returns a + session ID used for all subsequent operations. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + extra_headers = { + **strip_not_given( + { + "x-language": str(x_language) if is_given(x_language) else not_given, + "x-sdk-version": str(x_sdk_version) if is_given(x_sdk_version) else not_given, + "x-sent-at": str(x_sent_at) if is_given(x_sent_at) else not_given, + "x-stream-response": str(x_stream_response) if is_given(x_stream_response) else not_given, + } + ), + **(extra_headers or {}), + } + return self._post( + "/sessions/start", + body=maybe_transform(body, session_start_params.SessionStartParams), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=object, + ) + + +class AsyncSessionsResource(AsyncAPIResource): + @cached_property + def with_raw_response(self) -> AsyncSessionsResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/browserbase/stagehand-python#accessing-raw-response-data-eg-headers + """ + return AsyncSessionsResourceWithRawResponse(self) @cached_property - def with_streaming_response(self) -> SessionsResourceWithStreamingResponse: + def with_streaming_response(self) -> AsyncSessionsResourceWithStreamingResponse: """ An alternative to `.with_raw_response` that doesn't eagerly read the response body. For more information, see https://www.github.com/browserbase/stagehand-python#with_streaming_response """ - return SessionsResourceWithStreamingResponse(self) + return AsyncSessionsResourceWithStreamingResponse(self) - def start( + async def act( self, + id: object, *, body: object | Omit = omit, x_language: object | Omit = omit, @@ -55,10 +421,56 @@ def start( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> object: - """Creates a new browser session with the specified configuration. + """ + Executes a browser action using natural language instructions or a predefined + Action object. - Returns a - session ID used for all subsequent operations. + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + extra_headers = { + **strip_not_given( + { + "x-language": str(x_language) if is_given(x_language) else not_given, + "x-sdk-version": str(x_sdk_version) if is_given(x_sdk_version) else not_given, + "x-sent-at": str(x_sent_at) if is_given(x_sent_at) else not_given, + "x-stream-response": str(x_stream_response) if is_given(x_stream_response) else not_given, + } + ), + **(extra_headers or {}), + } + return await self._post( + f"/sessions/{id}/act", + body=await async_maybe_transform(body, session_act_params.SessionActParams), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=object, + ) + + async def end( + self, + id: object, + *, + x_language: object | Omit = omit, + x_sdk_version: object | Omit = omit, + x_sent_at: object | Omit = omit, + x_stream_response: object | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> object: + """ + Terminates the browser session and releases all associated resources. Args: extra_headers: Send extra headers @@ -80,35 +492,206 @@ def start( ), **(extra_headers or {}), } - return self._post( - "/sessions/start", - body=maybe_transform(body, session_start_params.SessionStartParams), + return await self._post( + f"/sessions/{id}/end", options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), cast_to=object, ) + async def execute_agent( + self, + id: object, + *, + body: object | Omit = omit, + x_language: object | Omit = omit, + x_sdk_version: object | Omit = omit, + x_sent_at: object | Omit = omit, + x_stream_response: object | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> object: + """ + Runs an autonomous AI agent that can perform complex multi-step browser tasks. + + Args: + extra_headers: Send extra headers -class AsyncSessionsResource(AsyncAPIResource): - @cached_property - def with_raw_response(self) -> AsyncSessionsResourceWithRawResponse: + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds """ - This property can be used as a prefix for any HTTP method call to return - the raw response object instead of the parsed content. + extra_headers = { + **strip_not_given( + { + "x-language": str(x_language) if is_given(x_language) else not_given, + "x-sdk-version": str(x_sdk_version) if is_given(x_sdk_version) else not_given, + "x-sent-at": str(x_sent_at) if is_given(x_sent_at) else not_given, + "x-stream-response": str(x_stream_response) if is_given(x_stream_response) else not_given, + } + ), + **(extra_headers or {}), + } + return await self._post( + f"/sessions/{id}/agentExecute", + body=await async_maybe_transform(body, session_execute_agent_params.SessionExecuteAgentParams), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=object, + ) - For more information, see https://www.github.com/browserbase/stagehand-python#accessing-raw-response-data-eg-headers + async def extract( + self, + id: object, + *, + body: object | Omit = omit, + x_language: object | Omit = omit, + x_sdk_version: object | Omit = omit, + x_sent_at: object | Omit = omit, + x_stream_response: object | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> object: """ - return AsyncSessionsResourceWithRawResponse(self) + Extracts structured data from the current page using AI-powered analysis. - @cached_property - def with_streaming_response(self) -> AsyncSessionsResourceWithStreamingResponse: + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds """ - An alternative to `.with_raw_response` that doesn't eagerly read the response body. + extra_headers = { + **strip_not_given( + { + "x-language": str(x_language) if is_given(x_language) else not_given, + "x-sdk-version": str(x_sdk_version) if is_given(x_sdk_version) else not_given, + "x-sent-at": str(x_sent_at) if is_given(x_sent_at) else not_given, + "x-stream-response": str(x_stream_response) if is_given(x_stream_response) else not_given, + } + ), + **(extra_headers or {}), + } + return await self._post( + f"/sessions/{id}/extract", + body=await async_maybe_transform(body, session_extract_params.SessionExtractParams), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=object, + ) - For more information, see https://www.github.com/browserbase/stagehand-python#with_streaming_response + async def navigate( + self, + id: object, + *, + body: object | Omit = omit, + x_language: object | Omit = omit, + x_sdk_version: object | Omit = omit, + x_sent_at: object | Omit = omit, + x_stream_response: object | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> object: """ - return AsyncSessionsResourceWithStreamingResponse(self) + Navigates the browser to the specified URL. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + extra_headers = { + **strip_not_given( + { + "x-language": str(x_language) if is_given(x_language) else not_given, + "x-sdk-version": str(x_sdk_version) if is_given(x_sdk_version) else not_given, + "x-sent-at": str(x_sent_at) if is_given(x_sent_at) else not_given, + "x-stream-response": str(x_stream_response) if is_given(x_stream_response) else not_given, + } + ), + **(extra_headers or {}), + } + return await self._post( + f"/sessions/{id}/navigate", + body=await async_maybe_transform(body, session_navigate_params.SessionNavigateParams), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=object, + ) + + async def observe( + self, + id: object, + *, + body: object | Omit = omit, + x_language: object | Omit = omit, + x_sdk_version: object | Omit = omit, + x_sent_at: object | Omit = omit, + x_stream_response: object | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> object: + """ + Identifies and returns available actions on the current page that match the + given instruction. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + extra_headers = { + **strip_not_given( + { + "x-language": str(x_language) if is_given(x_language) else not_given, + "x-sdk-version": str(x_sdk_version) if is_given(x_sdk_version) else not_given, + "x-sent-at": str(x_sent_at) if is_given(x_sent_at) else not_given, + "x-stream-response": str(x_stream_response) if is_given(x_stream_response) else not_given, + } + ), + **(extra_headers or {}), + } + return await self._post( + f"/sessions/{id}/observe", + body=await async_maybe_transform(body, session_observe_params.SessionObserveParams), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=object, + ) async def start( self, @@ -164,6 +747,24 @@ class SessionsResourceWithRawResponse: def __init__(self, sessions: SessionsResource) -> None: self._sessions = sessions + self.act = to_raw_response_wrapper( + sessions.act, + ) + self.end = to_raw_response_wrapper( + sessions.end, + ) + self.execute_agent = to_raw_response_wrapper( + sessions.execute_agent, + ) + self.extract = to_raw_response_wrapper( + sessions.extract, + ) + self.navigate = to_raw_response_wrapper( + sessions.navigate, + ) + self.observe = to_raw_response_wrapper( + sessions.observe, + ) self.start = to_raw_response_wrapper( sessions.start, ) @@ -173,6 +774,24 @@ class AsyncSessionsResourceWithRawResponse: def __init__(self, sessions: AsyncSessionsResource) -> None: self._sessions = sessions + self.act = async_to_raw_response_wrapper( + sessions.act, + ) + self.end = async_to_raw_response_wrapper( + sessions.end, + ) + self.execute_agent = async_to_raw_response_wrapper( + sessions.execute_agent, + ) + self.extract = async_to_raw_response_wrapper( + sessions.extract, + ) + self.navigate = async_to_raw_response_wrapper( + sessions.navigate, + ) + self.observe = async_to_raw_response_wrapper( + sessions.observe, + ) self.start = async_to_raw_response_wrapper( sessions.start, ) @@ -182,6 +801,24 @@ class SessionsResourceWithStreamingResponse: def __init__(self, sessions: SessionsResource) -> None: self._sessions = sessions + self.act = to_streamed_response_wrapper( + sessions.act, + ) + self.end = to_streamed_response_wrapper( + sessions.end, + ) + self.execute_agent = to_streamed_response_wrapper( + sessions.execute_agent, + ) + self.extract = to_streamed_response_wrapper( + sessions.extract, + ) + self.navigate = to_streamed_response_wrapper( + sessions.navigate, + ) + self.observe = to_streamed_response_wrapper( + sessions.observe, + ) self.start = to_streamed_response_wrapper( sessions.start, ) @@ -191,6 +828,24 @@ class AsyncSessionsResourceWithStreamingResponse: def __init__(self, sessions: AsyncSessionsResource) -> None: self._sessions = sessions + self.act = async_to_streamed_response_wrapper( + sessions.act, + ) + self.end = async_to_streamed_response_wrapper( + sessions.end, + ) + self.execute_agent = async_to_streamed_response_wrapper( + sessions.execute_agent, + ) + self.extract = async_to_streamed_response_wrapper( + sessions.extract, + ) + self.navigate = async_to_streamed_response_wrapper( + sessions.navigate, + ) + self.observe = async_to_streamed_response_wrapper( + sessions.observe, + ) self.start = async_to_streamed_response_wrapper( sessions.start, ) diff --git a/src/stagehand/types/__init__.py b/src/stagehand/types/__init__.py index 5cd88eeb..acf81383 100644 --- a/src/stagehand/types/__init__.py +++ b/src/stagehand/types/__init__.py @@ -2,4 +2,9 @@ from __future__ import annotations +from .session_act_params import SessionActParams as SessionActParams from .session_start_params import SessionStartParams as SessionStartParams +from .session_extract_params import SessionExtractParams as SessionExtractParams +from .session_observe_params import SessionObserveParams as SessionObserveParams +from .session_navigate_params import SessionNavigateParams as SessionNavigateParams +from .session_execute_agent_params import SessionExecuteAgentParams as SessionExecuteAgentParams diff --git a/src/stagehand/types/session_act_params.py b/src/stagehand/types/session_act_params.py new file mode 100644 index 00000000..6d23c710 --- /dev/null +++ b/src/stagehand/types/session_act_params.py @@ -0,0 +1,21 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Annotated, TypedDict + +from .._utils import PropertyInfo + +__all__ = ["SessionActParams"] + + +class SessionActParams(TypedDict, total=False): + body: object + + x_language: Annotated[object, PropertyInfo(alias="x-language")] + + x_sdk_version: Annotated[object, PropertyInfo(alias="x-sdk-version")] + + x_sent_at: Annotated[object, PropertyInfo(alias="x-sent-at")] + + x_stream_response: Annotated[object, PropertyInfo(alias="x-stream-response")] diff --git a/src/stagehand/types/session_execute_agent_params.py b/src/stagehand/types/session_execute_agent_params.py new file mode 100644 index 00000000..891e3908 --- /dev/null +++ b/src/stagehand/types/session_execute_agent_params.py @@ -0,0 +1,21 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Annotated, TypedDict + +from .._utils import PropertyInfo + +__all__ = ["SessionExecuteAgentParams"] + + +class SessionExecuteAgentParams(TypedDict, total=False): + body: object + + x_language: Annotated[object, PropertyInfo(alias="x-language")] + + x_sdk_version: Annotated[object, PropertyInfo(alias="x-sdk-version")] + + x_sent_at: Annotated[object, PropertyInfo(alias="x-sent-at")] + + x_stream_response: Annotated[object, PropertyInfo(alias="x-stream-response")] diff --git a/src/stagehand/types/session_extract_params.py b/src/stagehand/types/session_extract_params.py new file mode 100644 index 00000000..4c6be4e5 --- /dev/null +++ b/src/stagehand/types/session_extract_params.py @@ -0,0 +1,21 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Annotated, TypedDict + +from .._utils import PropertyInfo + +__all__ = ["SessionExtractParams"] + + +class SessionExtractParams(TypedDict, total=False): + body: object + + x_language: Annotated[object, PropertyInfo(alias="x-language")] + + x_sdk_version: Annotated[object, PropertyInfo(alias="x-sdk-version")] + + x_sent_at: Annotated[object, PropertyInfo(alias="x-sent-at")] + + x_stream_response: Annotated[object, PropertyInfo(alias="x-stream-response")] diff --git a/src/stagehand/types/session_navigate_params.py b/src/stagehand/types/session_navigate_params.py new file mode 100644 index 00000000..5ba72ba2 --- /dev/null +++ b/src/stagehand/types/session_navigate_params.py @@ -0,0 +1,21 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Annotated, TypedDict + +from .._utils import PropertyInfo + +__all__ = ["SessionNavigateParams"] + + +class SessionNavigateParams(TypedDict, total=False): + body: object + + x_language: Annotated[object, PropertyInfo(alias="x-language")] + + x_sdk_version: Annotated[object, PropertyInfo(alias="x-sdk-version")] + + x_sent_at: Annotated[object, PropertyInfo(alias="x-sent-at")] + + x_stream_response: Annotated[object, PropertyInfo(alias="x-stream-response")] diff --git a/src/stagehand/types/session_observe_params.py b/src/stagehand/types/session_observe_params.py new file mode 100644 index 00000000..82e9e560 --- /dev/null +++ b/src/stagehand/types/session_observe_params.py @@ -0,0 +1,21 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Annotated, TypedDict + +from .._utils import PropertyInfo + +__all__ = ["SessionObserveParams"] + + +class SessionObserveParams(TypedDict, total=False): + body: object + + x_language: Annotated[object, PropertyInfo(alias="x-language")] + + x_sdk_version: Annotated[object, PropertyInfo(alias="x-sdk-version")] + + x_sent_at: Annotated[object, PropertyInfo(alias="x-sent-at")] + + x_stream_response: Annotated[object, PropertyInfo(alias="x-stream-response")] diff --git a/tests/api_resources/test_sessions.py b/tests/api_resources/test_sessions.py index 1e0fe2e0..8e1ba863 100644 --- a/tests/api_resources/test_sessions.py +++ b/tests/api_resources/test_sessions.py @@ -16,6 +16,287 @@ class TestSessions: parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_act(self, client: Stagehand) -> None: + session = client.sessions.act( + id={}, + ) + assert_matches_type(object, session, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_act_with_all_params(self, client: Stagehand) -> None: + session = client.sessions.act( + id={}, + body={}, + x_language={}, + x_sdk_version={}, + x_sent_at={}, + x_stream_response={}, + ) + assert_matches_type(object, session, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_raw_response_act(self, client: Stagehand) -> None: + response = client.sessions.with_raw_response.act( + id={}, + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + session = response.parse() + assert_matches_type(object, session, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_streaming_response_act(self, client: Stagehand) -> None: + with client.sessions.with_streaming_response.act( + id={}, + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + session = response.parse() + assert_matches_type(object, session, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_end(self, client: Stagehand) -> None: + session = client.sessions.end( + id={}, + ) + assert_matches_type(object, session, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_end_with_all_params(self, client: Stagehand) -> None: + session = client.sessions.end( + id={}, + x_language={}, + x_sdk_version={}, + x_sent_at={}, + x_stream_response={}, + ) + assert_matches_type(object, session, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_raw_response_end(self, client: Stagehand) -> None: + response = client.sessions.with_raw_response.end( + id={}, + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + session = response.parse() + assert_matches_type(object, session, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_streaming_response_end(self, client: Stagehand) -> None: + with client.sessions.with_streaming_response.end( + id={}, + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + session = response.parse() + assert_matches_type(object, session, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_execute_agent(self, client: Stagehand) -> None: + session = client.sessions.execute_agent( + id={}, + ) + assert_matches_type(object, session, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_execute_agent_with_all_params(self, client: Stagehand) -> None: + session = client.sessions.execute_agent( + id={}, + body={}, + x_language={}, + x_sdk_version={}, + x_sent_at={}, + x_stream_response={}, + ) + assert_matches_type(object, session, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_raw_response_execute_agent(self, client: Stagehand) -> None: + response = client.sessions.with_raw_response.execute_agent( + id={}, + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + session = response.parse() + assert_matches_type(object, session, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_streaming_response_execute_agent(self, client: Stagehand) -> None: + with client.sessions.with_streaming_response.execute_agent( + id={}, + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + session = response.parse() + assert_matches_type(object, session, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_extract(self, client: Stagehand) -> None: + session = client.sessions.extract( + id={}, + ) + assert_matches_type(object, session, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_extract_with_all_params(self, client: Stagehand) -> None: + session = client.sessions.extract( + id={}, + body={}, + x_language={}, + x_sdk_version={}, + x_sent_at={}, + x_stream_response={}, + ) + assert_matches_type(object, session, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_raw_response_extract(self, client: Stagehand) -> None: + response = client.sessions.with_raw_response.extract( + id={}, + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + session = response.parse() + assert_matches_type(object, session, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_streaming_response_extract(self, client: Stagehand) -> None: + with client.sessions.with_streaming_response.extract( + id={}, + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + session = response.parse() + assert_matches_type(object, session, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_navigate(self, client: Stagehand) -> None: + session = client.sessions.navigate( + id={}, + ) + assert_matches_type(object, session, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_navigate_with_all_params(self, client: Stagehand) -> None: + session = client.sessions.navigate( + id={}, + body={}, + x_language={}, + x_sdk_version={}, + x_sent_at={}, + x_stream_response={}, + ) + assert_matches_type(object, session, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_raw_response_navigate(self, client: Stagehand) -> None: + response = client.sessions.with_raw_response.navigate( + id={}, + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + session = response.parse() + assert_matches_type(object, session, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_streaming_response_navigate(self, client: Stagehand) -> None: + with client.sessions.with_streaming_response.navigate( + id={}, + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + session = response.parse() + assert_matches_type(object, session, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_observe(self, client: Stagehand) -> None: + session = client.sessions.observe( + id={}, + ) + assert_matches_type(object, session, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_observe_with_all_params(self, client: Stagehand) -> None: + session = client.sessions.observe( + id={}, + body={}, + x_language={}, + x_sdk_version={}, + x_sent_at={}, + x_stream_response={}, + ) + assert_matches_type(object, session, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_raw_response_observe(self, client: Stagehand) -> None: + response = client.sessions.with_raw_response.observe( + id={}, + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + session = response.parse() + assert_matches_type(object, session, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_streaming_response_observe(self, client: Stagehand) -> None: + with client.sessions.with_streaming_response.observe( + id={}, + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + session = response.parse() + assert_matches_type(object, session, path=["response"]) + + assert cast(Any, response.is_closed) is True + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_method_start(self, client: Stagehand) -> None: @@ -62,6 +343,287 @@ class TestAsyncSessions: "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] ) + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_method_act(self, async_client: AsyncStagehand) -> None: + session = await async_client.sessions.act( + id={}, + ) + assert_matches_type(object, session, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_method_act_with_all_params(self, async_client: AsyncStagehand) -> None: + session = await async_client.sessions.act( + id={}, + body={}, + x_language={}, + x_sdk_version={}, + x_sent_at={}, + x_stream_response={}, + ) + assert_matches_type(object, session, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_raw_response_act(self, async_client: AsyncStagehand) -> None: + response = await async_client.sessions.with_raw_response.act( + id={}, + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + session = await response.parse() + assert_matches_type(object, session, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_streaming_response_act(self, async_client: AsyncStagehand) -> None: + async with async_client.sessions.with_streaming_response.act( + id={}, + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + session = await response.parse() + assert_matches_type(object, session, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_method_end(self, async_client: AsyncStagehand) -> None: + session = await async_client.sessions.end( + id={}, + ) + assert_matches_type(object, session, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_method_end_with_all_params(self, async_client: AsyncStagehand) -> None: + session = await async_client.sessions.end( + id={}, + x_language={}, + x_sdk_version={}, + x_sent_at={}, + x_stream_response={}, + ) + assert_matches_type(object, session, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_raw_response_end(self, async_client: AsyncStagehand) -> None: + response = await async_client.sessions.with_raw_response.end( + id={}, + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + session = await response.parse() + assert_matches_type(object, session, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_streaming_response_end(self, async_client: AsyncStagehand) -> None: + async with async_client.sessions.with_streaming_response.end( + id={}, + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + session = await response.parse() + assert_matches_type(object, session, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_method_execute_agent(self, async_client: AsyncStagehand) -> None: + session = await async_client.sessions.execute_agent( + id={}, + ) + assert_matches_type(object, session, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_method_execute_agent_with_all_params(self, async_client: AsyncStagehand) -> None: + session = await async_client.sessions.execute_agent( + id={}, + body={}, + x_language={}, + x_sdk_version={}, + x_sent_at={}, + x_stream_response={}, + ) + assert_matches_type(object, session, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_raw_response_execute_agent(self, async_client: AsyncStagehand) -> None: + response = await async_client.sessions.with_raw_response.execute_agent( + id={}, + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + session = await response.parse() + assert_matches_type(object, session, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_streaming_response_execute_agent(self, async_client: AsyncStagehand) -> None: + async with async_client.sessions.with_streaming_response.execute_agent( + id={}, + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + session = await response.parse() + assert_matches_type(object, session, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_method_extract(self, async_client: AsyncStagehand) -> None: + session = await async_client.sessions.extract( + id={}, + ) + assert_matches_type(object, session, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_method_extract_with_all_params(self, async_client: AsyncStagehand) -> None: + session = await async_client.sessions.extract( + id={}, + body={}, + x_language={}, + x_sdk_version={}, + x_sent_at={}, + x_stream_response={}, + ) + assert_matches_type(object, session, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_raw_response_extract(self, async_client: AsyncStagehand) -> None: + response = await async_client.sessions.with_raw_response.extract( + id={}, + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + session = await response.parse() + assert_matches_type(object, session, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_streaming_response_extract(self, async_client: AsyncStagehand) -> None: + async with async_client.sessions.with_streaming_response.extract( + id={}, + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + session = await response.parse() + assert_matches_type(object, session, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_method_navigate(self, async_client: AsyncStagehand) -> None: + session = await async_client.sessions.navigate( + id={}, + ) + assert_matches_type(object, session, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_method_navigate_with_all_params(self, async_client: AsyncStagehand) -> None: + session = await async_client.sessions.navigate( + id={}, + body={}, + x_language={}, + x_sdk_version={}, + x_sent_at={}, + x_stream_response={}, + ) + assert_matches_type(object, session, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_raw_response_navigate(self, async_client: AsyncStagehand) -> None: + response = await async_client.sessions.with_raw_response.navigate( + id={}, + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + session = await response.parse() + assert_matches_type(object, session, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_streaming_response_navigate(self, async_client: AsyncStagehand) -> None: + async with async_client.sessions.with_streaming_response.navigate( + id={}, + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + session = await response.parse() + assert_matches_type(object, session, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_method_observe(self, async_client: AsyncStagehand) -> None: + session = await async_client.sessions.observe( + id={}, + ) + assert_matches_type(object, session, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_method_observe_with_all_params(self, async_client: AsyncStagehand) -> None: + session = await async_client.sessions.observe( + id={}, + body={}, + x_language={}, + x_sdk_version={}, + x_sent_at={}, + x_stream_response={}, + ) + assert_matches_type(object, session, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_raw_response_observe(self, async_client: AsyncStagehand) -> None: + response = await async_client.sessions.with_raw_response.observe( + id={}, + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + session = await response.parse() + assert_matches_type(object, session, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_streaming_response_observe(self, async_client: AsyncStagehand) -> None: + async with async_client.sessions.with_streaming_response.observe( + id={}, + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + session = await response.parse() + assert_matches_type(object, session, path=["response"]) + + assert cast(Any, response.is_closed) is True + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_method_start(self, async_client: AsyncStagehand) -> None: From cf4d141e740440a53b942cc3fd3ebfc0c3f64e66 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 17 Dec 2025 05:27:56 +0000 Subject: [PATCH 30/88] chore: speedup initial import --- src/stagehand/_client.py | 88 +++++++++++++++++++++++++++++++--------- 1 file changed, 68 insertions(+), 20 deletions(-) diff --git a/src/stagehand/_client.py b/src/stagehand/_client.py index a8767a89..b0f365e6 100644 --- a/src/stagehand/_client.py +++ b/src/stagehand/_client.py @@ -3,7 +3,7 @@ from __future__ import annotations import os -from typing import Any, Mapping +from typing import TYPE_CHECKING, Any, Mapping from typing_extensions import Self, override import httpx @@ -20,8 +20,8 @@ not_given, ) from ._utils import is_given, get_async_library +from ._compat import cached_property from ._version import __version__ -from .resources import sessions from ._streaming import Stream as Stream, AsyncStream as AsyncStream from ._exceptions import APIStatusError, StagehandError from ._base_client import ( @@ -30,6 +30,10 @@ AsyncAPIClient, ) +if TYPE_CHECKING: + from .resources import sessions + from .resources.sessions import SessionsResource, AsyncSessionsResource + __all__ = [ "Timeout", "Transport", @@ -43,10 +47,6 @@ class Stagehand(SyncAPIClient): - sessions: sessions.SessionsResource - with_raw_response: StagehandWithRawResponse - with_streaming_response: StagehandWithStreamedResponse - # client options browserbase_api_key: str browserbase_project_id: str @@ -124,9 +124,19 @@ def __init__( _strict_response_validation=_strict_response_validation, ) - self.sessions = sessions.SessionsResource(self) - self.with_raw_response = StagehandWithRawResponse(self) - self.with_streaming_response = StagehandWithStreamedResponse(self) + @cached_property + def sessions(self) -> SessionsResource: + from .resources.sessions import SessionsResource + + return SessionsResource(self) + + @cached_property + def with_raw_response(self) -> StagehandWithRawResponse: + return StagehandWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> StagehandWithStreamedResponse: + return StagehandWithStreamedResponse(self) @property @override @@ -252,10 +262,6 @@ def _make_status_error( class AsyncStagehand(AsyncAPIClient): - sessions: sessions.AsyncSessionsResource - with_raw_response: AsyncStagehandWithRawResponse - with_streaming_response: AsyncStagehandWithStreamedResponse - # client options browserbase_api_key: str browserbase_project_id: str @@ -333,9 +339,19 @@ def __init__( _strict_response_validation=_strict_response_validation, ) - self.sessions = sessions.AsyncSessionsResource(self) - self.with_raw_response = AsyncStagehandWithRawResponse(self) - self.with_streaming_response = AsyncStagehandWithStreamedResponse(self) + @cached_property + def sessions(self) -> AsyncSessionsResource: + from .resources.sessions import AsyncSessionsResource + + return AsyncSessionsResource(self) + + @cached_property + def with_raw_response(self) -> AsyncStagehandWithRawResponse: + return AsyncStagehandWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncStagehandWithStreamedResponse: + return AsyncStagehandWithStreamedResponse(self) @property @override @@ -461,23 +477,55 @@ def _make_status_error( class StagehandWithRawResponse: + _client: Stagehand + def __init__(self, client: Stagehand) -> None: - self.sessions = sessions.SessionsResourceWithRawResponse(client.sessions) + self._client = client + + @cached_property + def sessions(self) -> sessions.SessionsResourceWithRawResponse: + from .resources.sessions import SessionsResourceWithRawResponse + + return SessionsResourceWithRawResponse(self._client.sessions) class AsyncStagehandWithRawResponse: + _client: AsyncStagehand + def __init__(self, client: AsyncStagehand) -> None: - self.sessions = sessions.AsyncSessionsResourceWithRawResponse(client.sessions) + self._client = client + + @cached_property + def sessions(self) -> sessions.AsyncSessionsResourceWithRawResponse: + from .resources.sessions import AsyncSessionsResourceWithRawResponse + + return AsyncSessionsResourceWithRawResponse(self._client.sessions) class StagehandWithStreamedResponse: + _client: Stagehand + def __init__(self, client: Stagehand) -> None: - self.sessions = sessions.SessionsResourceWithStreamingResponse(client.sessions) + self._client = client + + @cached_property + def sessions(self) -> sessions.SessionsResourceWithStreamingResponse: + from .resources.sessions import SessionsResourceWithStreamingResponse + + return SessionsResourceWithStreamingResponse(self._client.sessions) class AsyncStagehandWithStreamedResponse: + _client: AsyncStagehand + def __init__(self, client: AsyncStagehand) -> None: - self.sessions = sessions.AsyncSessionsResourceWithStreamingResponse(client.sessions) + self._client = client + + @cached_property + def sessions(self) -> sessions.AsyncSessionsResourceWithStreamingResponse: + from .resources.sessions import AsyncSessionsResourceWithStreamingResponse + + return AsyncSessionsResourceWithStreamingResponse(self._client.sessions) Client = Stagehand From a299d95d69991961202cc68f81680563370e0ebb Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 17 Dec 2025 05:34:39 +0000 Subject: [PATCH 31/88] refactor(internal): switch from rye to uv --- .devcontainer/Dockerfile | 3 +- .devcontainer/devcontainer.json | 4 +- .github/workflows/ci.yml | 39 +- .github/workflows/publish-pypi.yml | 11 +- Brewfile | 2 +- CONTRIBUTING.md | 22 +- bin/publish-pypi | 5 +- noxfile.py | 9 - pyproject.toml | 52 +- requirements-dev.lock | 157 +-- requirements.lock | 76 -- scripts/bootstrap | 11 +- scripts/format | 10 +- scripts/lint | 12 +- scripts/test | 31 +- uv.lock | 1829 ++++++++++++++++++++++++++++ 16 files changed, 1995 insertions(+), 278 deletions(-) delete mode 100644 noxfile.py delete mode 100644 requirements.lock create mode 100644 uv.lock diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index ff261bad..62c2d13f 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -3,7 +3,6 @@ FROM mcr.microsoft.com/vscode/devcontainers/python:0-${VARIANT} USER vscode -RUN curl -sSf https://rye.astral.sh/get | RYE_VERSION="0.44.0" RYE_INSTALL_OPTION="--yes" bash -ENV PATH=/home/vscode/.rye/shims:$PATH +COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/ RUN echo "[[ -d .venv ]] && source .venv/bin/activate || export PATH=\$PATH" >> /home/vscode/.bashrc diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index c17fdc16..e01283d8 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -7,7 +7,7 @@ "context": ".." }, - "postStartCommand": "rye sync --all-features", + "postStartCommand": "uv sync --all-extras", "customizations": { "vscode": { @@ -20,7 +20,7 @@ "python.defaultInterpreterPath": ".venv/bin/python", "python.typeChecking": "basic", "terminal.integrated.env.linux": { - "PATH": "/home/vscode/.rye/shims:${env:PATH}" + "PATH": "${env:PATH}" } } } diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index edab221c..66aa23d9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,16 +21,13 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Install Rye - run: | - curl -sSf https://rye.astral.sh/get | bash - echo "$HOME/.rye/shims" >> $GITHUB_PATH - env: - RYE_VERSION: '0.44.0' - RYE_INSTALL_OPTION: '--yes' + - name: Install uv + uses: astral-sh/setup-uv@v5 + with: + version: '0.9.13' - name: Install dependencies - run: rye sync --all-features + run: uv sync --all-extras - name: Run lints run: ./scripts/lint @@ -46,19 +43,16 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Install Rye - run: | - curl -sSf https://rye.astral.sh/get | bash - echo "$HOME/.rye/shims" >> $GITHUB_PATH - env: - RYE_VERSION: '0.44.0' - RYE_INSTALL_OPTION: '--yes' + - name: Install uv + uses: astral-sh/setup-uv@v5 + with: + version: '0.9.13' - name: Install dependencies - run: rye sync --all-features + run: uv sync --all-extras - name: Run build - run: rye build + run: uv build - name: Get GitHub OIDC Token if: github.repository == 'stainless-sdks/stagehand-python' @@ -83,13 +77,10 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Install Rye - run: | - curl -sSf https://rye.astral.sh/get | bash - echo "$HOME/.rye/shims" >> $GITHUB_PATH - env: - RYE_VERSION: '0.44.0' - RYE_INSTALL_OPTION: '--yes' + - name: Install uv + uses: astral-sh/setup-uv@v5 + with: + version: '0.9.13' - name: Bootstrap run: ./scripts/bootstrap diff --git a/.github/workflows/publish-pypi.yml b/.github/workflows/publish-pypi.yml index 0b389f9c..790bf4c3 100644 --- a/.github/workflows/publish-pypi.yml +++ b/.github/workflows/publish-pypi.yml @@ -16,13 +16,10 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Install Rye - run: | - curl -sSf https://rye.astral.sh/get | bash - echo "$HOME/.rye/shims" >> $GITHUB_PATH - env: - RYE_VERSION: '0.44.0' - RYE_INSTALL_OPTION: '--yes' + - name: Install uv + uses: astral-sh/setup-uv@v5 + with: + version: '0.9.13' - name: Publish to PyPI run: | diff --git a/Brewfile b/Brewfile index 492ca37b..c43041ce 100644 --- a/Brewfile +++ b/Brewfile @@ -1,2 +1,2 @@ -brew "rye" +brew "uv" diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 89d0b51f..f29de26b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,32 +1,32 @@ ## Setting up the environment -### With Rye +### With `uv` -We use [Rye](https://rye.astral.sh/) to manage dependencies because it will automatically provision a Python environment with the expected Python version. To set it up, run: +We use [uv](https://docs.astral.sh/uv/) to manage dependencies because it will automatically provision a Python environment with the expected Python version. To set it up, run: ```sh $ ./scripts/bootstrap ``` -Or [install Rye manually](https://rye.astral.sh/guide/installation/) and run: +Or [install uv manually](https://docs.astral.sh/uv/getting-started/installation/) and run: ```sh -$ rye sync --all-features +$ uv sync --all-extras ``` -You can then run scripts using `rye run python script.py` or by activating the virtual environment: +You can then run scripts using `uv run python script.py` or by manually activating the virtual environment: ```sh -# Activate the virtual environment - https://docs.python.org/3/library/venv.html#how-venvs-work +# manually activate - https://docs.python.org/3/library/venv.html#how-venvs-work $ source .venv/bin/activate -# now you can omit the `rye run` prefix +# now you can omit the `uv run` prefix $ python script.py ``` -### Without Rye +### Without `uv` -Alternatively if you don't want to install `Rye`, you can stick with the standard `pip` setup by ensuring you have the Python version specified in `.python-version`, create a virtual environment however you desire and then install dependencies using this command: +Alternatively if you don't want to install `uv`, you can stick with the standard `pip` setup by ensuring you have the Python version specified in `.python-version`, create a virtual environment however you desire and then install dependencies using this command: ```sh $ pip install -r requirements-dev.lock @@ -45,7 +45,7 @@ All files in the `examples/` directory are not modified by the generator and can ```py # add an example to examples/.py -#!/usr/bin/env -S rye run python +#!/usr/bin/env -S uv run python … ``` @@ -72,7 +72,7 @@ Building this package will create two files in the `dist/` directory, a `.tar.gz To create a distributable version of the library, all you have to do is run this command: ```sh -$ rye build +$ uv build # or $ python -m build ``` diff --git a/bin/publish-pypi b/bin/publish-pypi index 826054e9..e72ca2fa 100644 --- a/bin/publish-pypi +++ b/bin/publish-pypi @@ -1,6 +1,7 @@ #!/usr/bin/env bash set -eux +rm -rf dist mkdir -p dist -rye build --clean -rye publish --yes --token=$PYPI_TOKEN +uv build +uv publish --token=$PYPI_TOKEN diff --git a/noxfile.py b/noxfile.py deleted file mode 100644 index 53bca7ff..00000000 --- a/noxfile.py +++ /dev/null @@ -1,9 +0,0 @@ -import nox - - -@nox.session(reuse_venv=True, name="test-pydantic-v1") -def test_pydantic_v1(session: nox.Session) -> None: - session.install("-r", "requirements-dev.lock") - session.install("pydantic<2") - - session.run("pytest", "--showlocals", "--ignore=tests/functional", *session.posargs) diff --git a/pyproject.toml b/pyproject.toml index eb572236..a5160a3b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -43,10 +43,19 @@ Repository = "https://github.com/browserbase/stagehand-python" [project.optional-dependencies] aiohttp = ["aiohttp", "httpx_aiohttp>=0.1.9"] -[tool.rye] +[tool.uv] managed = true -# version pins are in requirements-dev.lock -dev-dependencies = [ +required-version = ">=0.9" +conflicts = [ + [ + { group = "pydantic-v1" }, + { group = "pydantic-v2" }, + ], +] + +[dependency-groups] +# version pins are in uv.lock +dev = [ "pyright==1.1.399", "mypy==1.17", "respx", @@ -54,41 +63,18 @@ dev-dependencies = [ "pytest-asyncio", "ruff", "time-machine", - "nox", "dirty-equals>=0.6.0", "importlib-metadata>=6.7.0", "rich>=13.7.1", "pytest-xdist>=3.6.1", ] - -[tool.rye.scripts] -format = { chain = [ - "format:ruff", - "format:docs", - "fix:ruff", - # run formatting again to fix any inconsistencies when imports are stripped - "format:ruff", -]} -"format:docs" = "python scripts/utils/ruffen-docs.py README.md api.md" -"format:ruff" = "ruff format" - -"lint" = { chain = [ - "check:ruff", - "typecheck", - "check:importable", -]} -"check:ruff" = "ruff check ." -"fix:ruff" = "ruff check --fix ." - -"check:importable" = "python -c 'import stagehand'" - -typecheck = { chain = [ - "typecheck:pyright", - "typecheck:mypy" -]} -"typecheck:pyright" = "pyright" -"typecheck:verify-types" = "pyright --verifytypes stagehand --ignoreexternal" -"typecheck:mypy" = "mypy ." +pydantic-v1 = [ + "pydantic>=1.9.0,<2", +] +pydantic-v2 = [ + "pydantic~=2.0 ; python_full_version < '3.14'", + "pydantic~=2.12 ; python_full_version >= '3.14'", +] [build-system] requires = ["hatchling==1.26.3", "hatch-fancy-pypi-readme"] diff --git a/requirements-dev.lock b/requirements-dev.lock index 5f6cc3bb..db709b17 100644 --- a/requirements-dev.lock +++ b/requirements-dev.lock @@ -1,149 +1,110 @@ -# generated by rye -# use `rye lock` or `rye sync` to update this lockfile -# -# last locked with the following flags: -# pre: false -# features: [] -# all-features: true -# with-sources: false -# generate-hashes: false -# universal: false - --e file:. -aiohappyeyeballs==2.6.1 - # via aiohttp -aiohttp==3.13.2 - # via httpx-aiohttp - # via stagehand -aiosignal==1.4.0 - # via aiohttp +# This file was autogenerated by uv via the following command: +# uv export -o requirements-dev.lock --no-hashes +-e . annotated-types==0.7.0 # via pydantic anyio==4.12.0 - # via httpx - # via stagehand -argcomplete==3.6.3 - # via nox -async-timeout==5.0.1 - # via aiohttp -attrs==25.4.0 - # via aiohttp - # via nox -backports-asyncio-runner==1.2.0 + # via + # httpx + # stagehand +backports-asyncio-runner==1.2.0 ; python_full_version < '3.11' # via pytest-asyncio certifi==2025.11.12 - # via httpcore - # via httpx -colorlog==6.10.1 - # via nox -dependency-groups==1.3.1 - # via nox + # via + # httpcore + # httpx +colorama==0.4.6 ; sys_platform == 'win32' + # via pytest dirty-equals==0.11 -distlib==0.4.0 - # via virtualenv distro==1.9.0 # via stagehand -exceptiongroup==1.3.1 - # via anyio - # via pytest +exceptiongroup==1.3.1 ; python_full_version < '3.11' + # via + # anyio + # pytest execnet==2.1.2 # via pytest-xdist -filelock==3.19.1 - # via virtualenv -frozenlist==1.8.0 - # via aiohttp - # via aiosignal h11==0.16.0 # via httpcore httpcore==1.0.9 # via httpx httpx==0.28.1 - # via httpx-aiohttp - # via respx - # via stagehand -httpx-aiohttp==0.1.9 - # via stagehand -humanize==4.13.0 - # via nox + # via + # respx + # stagehand idna==3.11 - # via anyio - # via httpx - # via yarl + # via + # anyio + # httpx importlib-metadata==8.7.0 -iniconfig==2.1.0 +iniconfig==2.1.0 ; python_full_version < '3.10' + # via pytest +iniconfig==2.3.0 ; python_full_version >= '3.10' # via pytest -markdown-it-py==3.0.0 +markdown-it-py==3.0.0 ; python_full_version < '3.10' + # via rich +markdown-it-py==4.0.0 ; python_full_version >= '3.10' # via rich mdurl==0.1.2 # via markdown-it-py -multidict==6.7.0 - # via aiohttp - # via yarl mypy==1.17.0 mypy-extensions==1.1.0 # via mypy nodeenv==1.9.1 # via pyright -nox==2025.11.12 packaging==25.0 - # via dependency-groups - # via nox # via pytest pathspec==0.12.1 # via mypy -platformdirs==4.4.0 - # via virtualenv pluggy==1.6.0 # via pytest -propcache==0.4.1 - # via aiohttp - # via yarl pydantic==2.12.5 # via stagehand pydantic-core==2.41.5 # via pydantic pygments==2.19.2 - # via pytest - # via rich + # via + # pytest + # rich pyright==1.1.399 -pytest==8.4.2 - # via pytest-asyncio - # via pytest-xdist -pytest-asyncio==1.2.0 +pytest==8.4.2 ; python_full_version < '3.10' + # via + # pytest-asyncio + # pytest-xdist +pytest==9.0.1 ; python_full_version >= '3.10' + # via + # pytest-asyncio + # pytest-xdist +pytest-asyncio==1.2.0 ; python_full_version < '3.10' +pytest-asyncio==1.3.0 ; python_full_version >= '3.10' pytest-xdist==3.8.0 -python-dateutil==2.9.0.post0 +python-dateutil==2.9.0.post0 ; python_full_version < '3.10' # via time-machine respx==0.22.0 rich==14.2.0 ruff==0.14.7 -six==1.17.0 +six==1.17.0 ; python_full_version < '3.10' # via python-dateutil sniffio==1.3.1 # via stagehand -time-machine==2.19.0 -tomli==2.3.0 - # via dependency-groups - # via mypy - # via nox - # via pytest +time-machine==2.19.0 ; python_full_version < '3.10' +time-machine==3.1.0 ; python_full_version >= '3.10' +tomli==2.3.0 ; python_full_version < '3.11' + # via + # mypy + # pytest typing-extensions==4.15.0 - # via aiosignal - # via anyio - # via exceptiongroup - # via multidict - # via mypy - # via pydantic - # via pydantic-core - # via pyright - # via pytest-asyncio - # via stagehand - # via typing-inspection - # via virtualenv + # via + # anyio + # exceptiongroup + # mypy + # pydantic + # pydantic-core + # pyright + # pytest-asyncio + # stagehand + # typing-inspection typing-inspection==0.4.2 # via pydantic -virtualenv==20.35.4 - # via nox -yarl==1.22.0 - # via aiohttp zipp==3.23.0 # via importlib-metadata diff --git a/requirements.lock b/requirements.lock deleted file mode 100644 index 3a9ba327..00000000 --- a/requirements.lock +++ /dev/null @@ -1,76 +0,0 @@ -# generated by rye -# use `rye lock` or `rye sync` to update this lockfile -# -# last locked with the following flags: -# pre: false -# features: [] -# all-features: true -# with-sources: false -# generate-hashes: false -# universal: false - --e file:. -aiohappyeyeballs==2.6.1 - # via aiohttp -aiohttp==3.13.2 - # via httpx-aiohttp - # via stagehand -aiosignal==1.4.0 - # via aiohttp -annotated-types==0.7.0 - # via pydantic -anyio==4.12.0 - # via httpx - # via stagehand -async-timeout==5.0.1 - # via aiohttp -attrs==25.4.0 - # via aiohttp -certifi==2025.11.12 - # via httpcore - # via httpx -distro==1.9.0 - # via stagehand -exceptiongroup==1.3.1 - # via anyio -frozenlist==1.8.0 - # via aiohttp - # via aiosignal -h11==0.16.0 - # via httpcore -httpcore==1.0.9 - # via httpx -httpx==0.28.1 - # via httpx-aiohttp - # via stagehand -httpx-aiohttp==0.1.9 - # via stagehand -idna==3.11 - # via anyio - # via httpx - # via yarl -multidict==6.7.0 - # via aiohttp - # via yarl -propcache==0.4.1 - # via aiohttp - # via yarl -pydantic==2.12.5 - # via stagehand -pydantic-core==2.41.5 - # via pydantic -sniffio==1.3.1 - # via stagehand -typing-extensions==4.15.0 - # via aiosignal - # via anyio - # via exceptiongroup - # via multidict - # via pydantic - # via pydantic-core - # via stagehand - # via typing-inspection -typing-inspection==0.4.2 - # via pydantic -yarl==1.22.0 - # via aiohttp diff --git a/scripts/bootstrap b/scripts/bootstrap index b430fee3..4638ec69 100755 --- a/scripts/bootstrap +++ b/scripts/bootstrap @@ -19,9 +19,12 @@ if [ -f "Brewfile" ] && [ "$(uname -s)" = "Darwin" ] && [ "$SKIP_BREW" != "1" ] } fi -echo "==> Installing Python dependencies…" +echo "==> Installing Python…" +uv python install -# experimental uv support makes installations significantly faster -rye config --set-bool behavior.use-uv=true +echo "==> Installing Python dependencies…" +uv sync --all-extras -rye sync --all-features +echo "==> Exporting Python dependencies…" +# note: `--no-hashes` is required because of https://github.com/pypa/pip/issues/4995 +uv export -o requirements-dev.lock --no-hashes diff --git a/scripts/format b/scripts/format index 667ec2d7..1d2f9c6e 100755 --- a/scripts/format +++ b/scripts/format @@ -4,5 +4,11 @@ set -e cd "$(dirname "$0")/.." -echo "==> Running formatters" -rye run format +echo "==> Running ruff" +uv run ruff format +uv run ruff check --fix . +# run formatting again to fix any inconsistencies when imports are stripped +uv run ruff format + +echo "==> Formatting docs" +uv run python scripts/utils/ruffen-docs.py README.md api.md diff --git a/scripts/lint b/scripts/lint index 6c3a7b4e..976e1608 100755 --- a/scripts/lint +++ b/scripts/lint @@ -4,8 +4,14 @@ set -e cd "$(dirname "$0")/.." -echo "==> Running lints" -rye run lint +echo "==> Running ruff" +uv run ruff check . + +echo "==> Running pyright" +uv run pyright + +echo "==> Running mypy" +uv run mypy . echo "==> Making sure it imports" -rye run python -c 'import stagehand' +uv run python -c 'import stagehand' diff --git a/scripts/test b/scripts/test index dbeda2d2..b56970b7 100755 --- a/scripts/test +++ b/scripts/test @@ -54,8 +54,31 @@ fi export DEFER_PYDANTIC_BUILD=false -echo "==> Running tests" -rye run pytest "$@" +# Note that we need to specify the patch version here so that uv +# won't use unstable (alpha, beta, rc) releases for the tests +PY_VERSION_MIN=">=3.9.0" +PY_VERSION_MAX=">=3.14.0" -echo "==> Running Pydantic v1 tests" -rye run nox -s test-pydantic-v1 -- "$@" +function run_tests() { + echo "==> Running tests with Pydantic v2" + uv run --isolated --all-extras pytest "$@" + + # Skip Pydantic v1 tests on latest Python (not supported) + if [[ "$UV_PYTHON" != "$PY_VERSION_MAX" ]]; then + echo "==> Running tests with Pydantic v1" + uv run --isolated --all-extras --group=pydantic-v1 pytest "$@" + fi +} + +# If UV_PYTHON is already set in the environment, just run the command once +if [[ -n "$UV_PYTHON" ]]; then + run_tests "$@" +else + # If UV_PYTHON is not set, run the command for min and max versions + + echo "==> Running tests for Python $PY_VERSION_MIN" + UV_PYTHON="$PY_VERSION_MIN" run_tests "$@" + + echo "==> Running tests for Python $PY_VERSION_MAX" + UV_PYTHON="$PY_VERSION_MAX" run_tests "$@" +fi diff --git a/uv.lock b/uv.lock new file mode 100644 index 00000000..69a70754 --- /dev/null +++ b/uv.lock @@ -0,0 +1,1829 @@ +version = 1 +revision = 3 +requires-python = ">=3.9" +resolution-markers = [ + "python_full_version >= '3.14' and extra != 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2'", + "python_full_version >= '3.10' and python_full_version < '3.14' and extra != 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2'", + "python_full_version < '3.10' and extra != 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2'", + "python_full_version >= '3.10' and extra == 'group-9-stagehand-pydantic-v1' and extra != 'group-9-stagehand-pydantic-v2'", + "python_full_version < '3.10' and extra == 'group-9-stagehand-pydantic-v1' and extra != 'group-9-stagehand-pydantic-v2'", + "python_full_version >= '3.10' and extra != 'group-9-stagehand-pydantic-v1' and extra != 'group-9-stagehand-pydantic-v2'", + "python_full_version < '3.10' and extra != 'group-9-stagehand-pydantic-v1' and extra != 'group-9-stagehand-pydantic-v2'", +] +conflicts = [[ + { package = "stagehand", group = "pydantic-v1" }, + { package = "stagehand", group = "pydantic-v2" }, +]] + +[[package]] +name = "aiohappyeyeballs" +version = "2.6.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/26/30/f84a107a9c4331c14b2b586036f40965c128aa4fee4dda5d3d51cb14ad54/aiohappyeyeballs-2.6.1.tar.gz", hash = "sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558", size = 22760, upload-time = "2025-03-12T01:42:48.764Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl", hash = "sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8", size = 15265, upload-time = "2025-03-12T01:42:47.083Z" }, +] + +[[package]] +name = "aiohttp" +version = "3.13.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohappyeyeballs" }, + { name = "aiosignal" }, + { name = "async-timeout", marker = "python_full_version < '3.11' or (extra == 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2')" }, + { name = "attrs" }, + { name = "frozenlist" }, + { name = "multidict" }, + { name = "propcache" }, + { name = "yarl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1c/ce/3b83ebba6b3207a7135e5fcaba49706f8a4b6008153b4e30540c982fae26/aiohttp-3.13.2.tar.gz", hash = "sha256:40176a52c186aefef6eb3cad2cdd30cd06e3afbe88fe8ab2af9c0b90f228daca", size = 7837994, upload-time = "2025-10-28T20:59:39.937Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6d/34/939730e66b716b76046dedfe0842995842fa906ccc4964bba414ff69e429/aiohttp-3.13.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:2372b15a5f62ed37789a6b383ff7344fc5b9f243999b0cd9b629d8bc5f5b4155", size = 736471, upload-time = "2025-10-28T20:55:27.924Z" }, + { url = "https://files.pythonhosted.org/packages/fd/cf/dcbdf2df7f6ca72b0bb4c0b4509701f2d8942cf54e29ca197389c214c07f/aiohttp-3.13.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e7f8659a48995edee7229522984bd1009c1213929c769c2daa80b40fe49a180c", size = 493985, upload-time = "2025-10-28T20:55:29.456Z" }, + { url = "https://files.pythonhosted.org/packages/9d/87/71c8867e0a1d0882dcbc94af767784c3cb381c1c4db0943ab4aae4fed65e/aiohttp-3.13.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:939ced4a7add92296b0ad38892ce62b98c619288a081170695c6babe4f50e636", size = 489274, upload-time = "2025-10-28T20:55:31.134Z" }, + { url = "https://files.pythonhosted.org/packages/38/0f/46c24e8dae237295eaadd113edd56dee96ef6462adf19b88592d44891dc5/aiohttp-3.13.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6315fb6977f1d0dd41a107c527fee2ed5ab0550b7d885bc15fee20ccb17891da", size = 1668171, upload-time = "2025-10-28T20:55:36.065Z" }, + { url = "https://files.pythonhosted.org/packages/eb/c6/4cdfb4440d0e28483681a48f69841fa5e39366347d66ef808cbdadddb20e/aiohttp-3.13.2-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6e7352512f763f760baaed2637055c49134fd1d35b37c2dedfac35bfe5cf8725", size = 1636036, upload-time = "2025-10-28T20:55:37.576Z" }, + { url = "https://files.pythonhosted.org/packages/84/37/8708cf678628216fb678ab327a4e1711c576d6673998f4f43e86e9ae90dd/aiohttp-3.13.2-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e09a0a06348a2dd73e7213353c90d709502d9786219f69b731f6caa0efeb46f5", size = 1727975, upload-time = "2025-10-28T20:55:39.457Z" }, + { url = "https://files.pythonhosted.org/packages/e6/2e/3ebfe12fdcb9b5f66e8a0a42dffcd7636844c8a018f261efb2419f68220b/aiohttp-3.13.2-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a09a6d073fb5789456545bdee2474d14395792faa0527887f2f4ec1a486a59d3", size = 1815823, upload-time = "2025-10-28T20:55:40.958Z" }, + { url = "https://files.pythonhosted.org/packages/a1/4f/ca2ef819488cbb41844c6cf92ca6dd15b9441e6207c58e5ae0e0fc8d70ad/aiohttp-3.13.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b59d13c443f8e049d9e94099c7e412e34610f1f49be0f230ec656a10692a5802", size = 1669374, upload-time = "2025-10-28T20:55:42.745Z" }, + { url = "https://files.pythonhosted.org/packages/f8/fe/1fe2e1179a0d91ce09c99069684aab619bf2ccde9b20bd6ca44f8837203e/aiohttp-3.13.2-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:20db2d67985d71ca033443a1ba2001c4b5693fe09b0e29f6d9358a99d4d62a8a", size = 1555315, upload-time = "2025-10-28T20:55:44.264Z" }, + { url = "https://files.pythonhosted.org/packages/5a/2b/f3781899b81c45d7cbc7140cddb8a3481c195e7cbff8e36374759d2ab5a5/aiohttp-3.13.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:960c2fc686ba27b535f9fd2b52d87ecd7e4fd1cf877f6a5cba8afb5b4a8bd204", size = 1639140, upload-time = "2025-10-28T20:55:46.626Z" }, + { url = "https://files.pythonhosted.org/packages/72/27/c37e85cd3ece6f6c772e549bd5a253d0c122557b25855fb274224811e4f2/aiohttp-3.13.2-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:6c00dbcf5f0d88796151e264a8eab23de2997c9303dd7c0bf622e23b24d3ce22", size = 1645496, upload-time = "2025-10-28T20:55:48.933Z" }, + { url = "https://files.pythonhosted.org/packages/66/20/3af1ab663151bd3780b123e907761cdb86ec2c4e44b2d9b195ebc91fbe37/aiohttp-3.13.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:fed38a5edb7945f4d1bcabe2fcd05db4f6ec7e0e82560088b754f7e08d93772d", size = 1697625, upload-time = "2025-10-28T20:55:50.377Z" }, + { url = "https://files.pythonhosted.org/packages/95/eb/ae5cab15efa365e13d56b31b0d085a62600298bf398a7986f8388f73b598/aiohttp-3.13.2-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:b395bbca716c38bef3c764f187860e88c724b342c26275bc03e906142fc5964f", size = 1542025, upload-time = "2025-10-28T20:55:51.861Z" }, + { url = "https://files.pythonhosted.org/packages/e9/2d/1683e8d67ec72d911397fe4e575688d2a9b8f6a6e03c8fdc9f3fd3d4c03f/aiohttp-3.13.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:204ffff2426c25dfda401ba08da85f9c59525cdc42bda26660463dd1cbcfec6f", size = 1714918, upload-time = "2025-10-28T20:55:53.515Z" }, + { url = "https://files.pythonhosted.org/packages/99/a2/ffe8e0e1c57c5e542d47ffa1fcf95ef2b3ea573bf7c4d2ee877252431efc/aiohttp-3.13.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:05c4dd3c48fb5f15db31f57eb35374cb0c09afdde532e7fb70a75aede0ed30f6", size = 1656113, upload-time = "2025-10-28T20:55:55.438Z" }, + { url = "https://files.pythonhosted.org/packages/0d/42/d511aff5c3a2b06c09d7d214f508a4ad8ac7799817f7c3d23e7336b5e896/aiohttp-3.13.2-cp310-cp310-win32.whl", hash = "sha256:e574a7d61cf10351d734bcddabbe15ede0eaa8a02070d85446875dc11189a251", size = 432290, upload-time = "2025-10-28T20:55:56.96Z" }, + { url = "https://files.pythonhosted.org/packages/8b/ea/1c2eb7098b5bad4532994f2b7a8228d27674035c9b3234fe02c37469ef14/aiohttp-3.13.2-cp310-cp310-win_amd64.whl", hash = "sha256:364f55663085d658b8462a1c3f17b2b84a5c2e1ba858e1b79bff7b2e24ad1514", size = 455075, upload-time = "2025-10-28T20:55:58.373Z" }, + { url = "https://files.pythonhosted.org/packages/35/74/b321e7d7ca762638cdf8cdeceb39755d9c745aff7a64c8789be96ddf6e96/aiohttp-3.13.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:4647d02df098f6434bafd7f32ad14942f05a9caa06c7016fdcc816f343997dd0", size = 743409, upload-time = "2025-10-28T20:56:00.354Z" }, + { url = "https://files.pythonhosted.org/packages/99/3d/91524b905ec473beaf35158d17f82ef5a38033e5809fe8742e3657cdbb97/aiohttp-3.13.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e3403f24bcb9c3b29113611c3c16a2a447c3953ecf86b79775e7be06f7ae7ccb", size = 497006, upload-time = "2025-10-28T20:56:01.85Z" }, + { url = "https://files.pythonhosted.org/packages/eb/d3/7f68bc02a67716fe80f063e19adbd80a642e30682ce74071269e17d2dba1/aiohttp-3.13.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:43dff14e35aba17e3d6d5ba628858fb8cb51e30f44724a2d2f0c75be492c55e9", size = 493195, upload-time = "2025-10-28T20:56:03.314Z" }, + { url = "https://files.pythonhosted.org/packages/98/31/913f774a4708775433b7375c4f867d58ba58ead833af96c8af3621a0d243/aiohttp-3.13.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e2a9ea08e8c58bb17655630198833109227dea914cd20be660f52215f6de5613", size = 1747759, upload-time = "2025-10-28T20:56:04.904Z" }, + { url = "https://files.pythonhosted.org/packages/e8/63/04efe156f4326f31c7c4a97144f82132c3bb21859b7bb84748d452ccc17c/aiohttp-3.13.2-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:53b07472f235eb80e826ad038c9d106c2f653584753f3ddab907c83f49eedead", size = 1704456, upload-time = "2025-10-28T20:56:06.986Z" }, + { url = "https://files.pythonhosted.org/packages/8e/02/4e16154d8e0a9cf4ae76f692941fd52543bbb148f02f098ca73cab9b1c1b/aiohttp-3.13.2-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e736c93e9c274fce6419af4aac199984d866e55f8a4cec9114671d0ea9688780", size = 1807572, upload-time = "2025-10-28T20:56:08.558Z" }, + { url = "https://files.pythonhosted.org/packages/34/58/b0583defb38689e7f06798f0285b1ffb3a6fb371f38363ce5fd772112724/aiohttp-3.13.2-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ff5e771f5dcbc81c64898c597a434f7682f2259e0cd666932a913d53d1341d1a", size = 1895954, upload-time = "2025-10-28T20:56:10.545Z" }, + { url = "https://files.pythonhosted.org/packages/6b/f3/083907ee3437425b4e376aa58b2c915eb1a33703ec0dc30040f7ae3368c6/aiohttp-3.13.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a3b6fb0c207cc661fa0bf8c66d8d9b657331ccc814f4719468af61034b478592", size = 1747092, upload-time = "2025-10-28T20:56:12.118Z" }, + { url = "https://files.pythonhosted.org/packages/ac/61/98a47319b4e425cc134e05e5f3fc512bf9a04bf65aafd9fdcda5d57ec693/aiohttp-3.13.2-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:97a0895a8e840ab3520e2288db7cace3a1981300d48babeb50e7425609e2e0ab", size = 1606815, upload-time = "2025-10-28T20:56:14.191Z" }, + { url = "https://files.pythonhosted.org/packages/97/4b/e78b854d82f66bb974189135d31fce265dee0f5344f64dd0d345158a5973/aiohttp-3.13.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9e8f8afb552297aca127c90cb840e9a1d4bfd6a10d7d8f2d9176e1acc69bad30", size = 1723789, upload-time = "2025-10-28T20:56:16.101Z" }, + { url = "https://files.pythonhosted.org/packages/ed/fc/9d2ccc794fc9b9acd1379d625c3a8c64a45508b5091c546dea273a41929e/aiohttp-3.13.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:ed2f9c7216e53c3df02264f25d824b079cc5914f9e2deba94155190ef648ee40", size = 1718104, upload-time = "2025-10-28T20:56:17.655Z" }, + { url = "https://files.pythonhosted.org/packages/66/65/34564b8765ea5c7d79d23c9113135d1dd3609173da13084830f1507d56cf/aiohttp-3.13.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:99c5280a329d5fa18ef30fd10c793a190d996567667908bef8a7f81f8202b948", size = 1785584, upload-time = "2025-10-28T20:56:19.238Z" }, + { url = "https://files.pythonhosted.org/packages/30/be/f6a7a426e02fc82781afd62016417b3948e2207426d90a0e478790d1c8a4/aiohttp-3.13.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:2ca6ffef405fc9c09a746cb5d019c1672cd7f402542e379afc66b370833170cf", size = 1595126, upload-time = "2025-10-28T20:56:20.836Z" }, + { url = "https://files.pythonhosted.org/packages/e5/c7/8e22d5d28f94f67d2af496f14a83b3c155d915d1fe53d94b66d425ec5b42/aiohttp-3.13.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:47f438b1a28e926c37632bff3c44df7d27c9b57aaf4e34b1def3c07111fdb782", size = 1800665, upload-time = "2025-10-28T20:56:22.922Z" }, + { url = "https://files.pythonhosted.org/packages/d1/11/91133c8b68b1da9fc16555706aa7276fdf781ae2bb0876c838dd86b8116e/aiohttp-3.13.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9acda8604a57bb60544e4646a4615c1866ee6c04a8edef9b8ee6fd1d8fa2ddc8", size = 1739532, upload-time = "2025-10-28T20:56:25.924Z" }, + { url = "https://files.pythonhosted.org/packages/17/6b/3747644d26a998774b21a616016620293ddefa4d63af6286f389aedac844/aiohttp-3.13.2-cp311-cp311-win32.whl", hash = "sha256:868e195e39b24aaa930b063c08bb0c17924899c16c672a28a65afded9c46c6ec", size = 431876, upload-time = "2025-10-28T20:56:27.524Z" }, + { url = "https://files.pythonhosted.org/packages/c3/63/688462108c1a00eb9f05765331c107f95ae86f6b197b865d29e930b7e462/aiohttp-3.13.2-cp311-cp311-win_amd64.whl", hash = "sha256:7fd19df530c292542636c2a9a85854fab93474396a52f1695e799186bbd7f24c", size = 456205, upload-time = "2025-10-28T20:56:29.062Z" }, + { url = "https://files.pythonhosted.org/packages/29/9b/01f00e9856d0a73260e86dd8ed0c2234a466c5c1712ce1c281548df39777/aiohttp-3.13.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:b1e56bab2e12b2b9ed300218c351ee2a3d8c8fdab5b1ec6193e11a817767e47b", size = 737623, upload-time = "2025-10-28T20:56:30.797Z" }, + { url = "https://files.pythonhosted.org/packages/5a/1b/4be39c445e2b2bd0aab4ba736deb649fabf14f6757f405f0c9685019b9e9/aiohttp-3.13.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:364e25edaabd3d37b1db1f0cbcee8c73c9a3727bfa262b83e5e4cf3489a2a9dc", size = 492664, upload-time = "2025-10-28T20:56:32.708Z" }, + { url = "https://files.pythonhosted.org/packages/28/66/d35dcfea8050e131cdd731dff36434390479b4045a8d0b9d7111b0a968f1/aiohttp-3.13.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c5c94825f744694c4b8db20b71dba9a257cd2ba8e010a803042123f3a25d50d7", size = 491808, upload-time = "2025-10-28T20:56:34.57Z" }, + { url = "https://files.pythonhosted.org/packages/00/29/8e4609b93e10a853b65f8291e64985de66d4f5848c5637cddc70e98f01f8/aiohttp-3.13.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ba2715d842ffa787be87cbfce150d5e88c87a98e0b62e0f5aa489169a393dbbb", size = 1738863, upload-time = "2025-10-28T20:56:36.377Z" }, + { url = "https://files.pythonhosted.org/packages/9d/fa/4ebdf4adcc0def75ced1a0d2d227577cd7b1b85beb7edad85fcc87693c75/aiohttp-3.13.2-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:585542825c4bc662221fb257889e011a5aa00f1ae4d75d1d246a5225289183e3", size = 1700586, upload-time = "2025-10-28T20:56:38.034Z" }, + { url = "https://files.pythonhosted.org/packages/da/04/73f5f02ff348a3558763ff6abe99c223381b0bace05cd4530a0258e52597/aiohttp-3.13.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:39d02cb6025fe1aabca329c5632f48c9532a3dabccd859e7e2f110668972331f", size = 1768625, upload-time = "2025-10-28T20:56:39.75Z" }, + { url = "https://files.pythonhosted.org/packages/f8/49/a825b79ffec124317265ca7d2344a86bcffeb960743487cb11988ffb3494/aiohttp-3.13.2-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e67446b19e014d37342f7195f592a2a948141d15a312fe0e700c2fd2f03124f6", size = 1867281, upload-time = "2025-10-28T20:56:41.471Z" }, + { url = "https://files.pythonhosted.org/packages/b9/48/adf56e05f81eac31edcfae45c90928f4ad50ef2e3ea72cb8376162a368f8/aiohttp-3.13.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4356474ad6333e41ccefd39eae869ba15a6c5299c9c01dfdcfdd5c107be4363e", size = 1752431, upload-time = "2025-10-28T20:56:43.162Z" }, + { url = "https://files.pythonhosted.org/packages/30/ab/593855356eead019a74e862f21523db09c27f12fd24af72dbc3555b9bfd9/aiohttp-3.13.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:eeacf451c99b4525f700f078becff32c32ec327b10dcf31306a8a52d78166de7", size = 1562846, upload-time = "2025-10-28T20:56:44.85Z" }, + { url = "https://files.pythonhosted.org/packages/39/0f/9f3d32271aa8dc35036e9668e31870a9d3b9542dd6b3e2c8a30931cb27ae/aiohttp-3.13.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d8a9b889aeabd7a4e9af0b7f4ab5ad94d42e7ff679aaec6d0db21e3b639ad58d", size = 1699606, upload-time = "2025-10-28T20:56:46.519Z" }, + { url = "https://files.pythonhosted.org/packages/2c/3c/52d2658c5699b6ef7692a3f7128b2d2d4d9775f2a68093f74bca06cf01e1/aiohttp-3.13.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:fa89cb11bc71a63b69568d5b8a25c3ca25b6d54c15f907ca1c130d72f320b76b", size = 1720663, upload-time = "2025-10-28T20:56:48.528Z" }, + { url = "https://files.pythonhosted.org/packages/9b/d4/8f8f3ff1fb7fb9e3f04fcad4e89d8a1cd8fc7d05de67e3de5b15b33008ff/aiohttp-3.13.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8aa7c807df234f693fed0ecd507192fc97692e61fee5702cdc11155d2e5cadc8", size = 1737939, upload-time = "2025-10-28T20:56:50.77Z" }, + { url = "https://files.pythonhosted.org/packages/03/d3/ddd348f8a27a634daae39a1b8e291ff19c77867af438af844bf8b7e3231b/aiohttp-3.13.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:9eb3e33fdbe43f88c3c75fa608c25e7c47bbd80f48d012763cb67c47f39a7e16", size = 1555132, upload-time = "2025-10-28T20:56:52.568Z" }, + { url = "https://files.pythonhosted.org/packages/39/b8/46790692dc46218406f94374903ba47552f2f9f90dad554eed61bfb7b64c/aiohttp-3.13.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:9434bc0d80076138ea986833156c5a48c9c7a8abb0c96039ddbb4afc93184169", size = 1764802, upload-time = "2025-10-28T20:56:54.292Z" }, + { url = "https://files.pythonhosted.org/packages/ba/e4/19ce547b58ab2a385e5f0b8aa3db38674785085abcf79b6e0edd1632b12f/aiohttp-3.13.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ff15c147b2ad66da1f2cbb0622313f2242d8e6e8f9b79b5206c84523a4473248", size = 1719512, upload-time = "2025-10-28T20:56:56.428Z" }, + { url = "https://files.pythonhosted.org/packages/70/30/6355a737fed29dcb6dfdd48682d5790cb5eab050f7b4e01f49b121d3acad/aiohttp-3.13.2-cp312-cp312-win32.whl", hash = "sha256:27e569eb9d9e95dbd55c0fc3ec3a9335defbf1d8bc1d20171a49f3c4c607b93e", size = 426690, upload-time = "2025-10-28T20:56:58.736Z" }, + { url = "https://files.pythonhosted.org/packages/0a/0d/b10ac09069973d112de6ef980c1f6bb31cb7dcd0bc363acbdad58f927873/aiohttp-3.13.2-cp312-cp312-win_amd64.whl", hash = "sha256:8709a0f05d59a71f33fd05c17fc11fcb8c30140506e13c2f5e8ee1b8964e1b45", size = 453465, upload-time = "2025-10-28T20:57:00.795Z" }, + { url = "https://files.pythonhosted.org/packages/bf/78/7e90ca79e5aa39f9694dcfd74f4720782d3c6828113bb1f3197f7e7c4a56/aiohttp-3.13.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:7519bdc7dfc1940d201651b52bf5e03f5503bda45ad6eacf64dda98be5b2b6be", size = 732139, upload-time = "2025-10-28T20:57:02.455Z" }, + { url = "https://files.pythonhosted.org/packages/db/ed/1f59215ab6853fbaa5c8495fa6cbc39edfc93553426152b75d82a5f32b76/aiohttp-3.13.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:088912a78b4d4f547a1f19c099d5a506df17eacec3c6f4375e2831ec1d995742", size = 490082, upload-time = "2025-10-28T20:57:04.784Z" }, + { url = "https://files.pythonhosted.org/packages/68/7b/fe0fe0f5e05e13629d893c760465173a15ad0039c0a5b0d0040995c8075e/aiohttp-3.13.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5276807b9de9092af38ed23ce120539ab0ac955547b38563a9ba4f5b07b95293", size = 489035, upload-time = "2025-10-28T20:57:06.894Z" }, + { url = "https://files.pythonhosted.org/packages/d2/04/db5279e38471b7ac801d7d36a57d1230feeee130bbe2a74f72731b23c2b1/aiohttp-3.13.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1237c1375eaef0db4dcd7c2559f42e8af7b87ea7d295b118c60c36a6e61cb811", size = 1720387, upload-time = "2025-10-28T20:57:08.685Z" }, + { url = "https://files.pythonhosted.org/packages/31/07/8ea4326bd7dae2bd59828f69d7fdc6e04523caa55e4a70f4a8725a7e4ed2/aiohttp-3.13.2-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:96581619c57419c3d7d78703d5b78c1e5e5fc0172d60f555bdebaced82ded19a", size = 1688314, upload-time = "2025-10-28T20:57:10.693Z" }, + { url = "https://files.pythonhosted.org/packages/48/ab/3d98007b5b87ffd519d065225438cc3b668b2f245572a8cb53da5dd2b1bc/aiohttp-3.13.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a2713a95b47374169409d18103366de1050fe0ea73db358fc7a7acb2880422d4", size = 1756317, upload-time = "2025-10-28T20:57:12.563Z" }, + { url = "https://files.pythonhosted.org/packages/97/3d/801ca172b3d857fafb7b50c7c03f91b72b867a13abca982ed6b3081774ef/aiohttp-3.13.2-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:228a1cd556b3caca590e9511a89444925da87d35219a49ab5da0c36d2d943a6a", size = 1858539, upload-time = "2025-10-28T20:57:14.623Z" }, + { url = "https://files.pythonhosted.org/packages/f7/0d/4764669bdf47bd472899b3d3db91fffbe925c8e3038ec591a2fd2ad6a14d/aiohttp-3.13.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ac6cde5fba8d7d8c6ac963dbb0256a9854e9fafff52fbcc58fdf819357892c3e", size = 1739597, upload-time = "2025-10-28T20:57:16.399Z" }, + { url = "https://files.pythonhosted.org/packages/c4/52/7bd3c6693da58ba16e657eb904a5b6decfc48ecd06e9ac098591653b1566/aiohttp-3.13.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f2bef8237544f4e42878c61cef4e2839fee6346dc60f5739f876a9c50be7fcdb", size = 1555006, upload-time = "2025-10-28T20:57:18.288Z" }, + { url = "https://files.pythonhosted.org/packages/48/30/9586667acec5993b6f41d2ebcf96e97a1255a85f62f3c653110a5de4d346/aiohttp-3.13.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:16f15a4eac3bc2d76c45f7ebdd48a65d41b242eb6c31c2245463b40b34584ded", size = 1683220, upload-time = "2025-10-28T20:57:20.241Z" }, + { url = "https://files.pythonhosted.org/packages/71/01/3afe4c96854cfd7b30d78333852e8e851dceaec1c40fd00fec90c6402dd2/aiohttp-3.13.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:bb7fb776645af5cc58ab804c58d7eba545a97e047254a52ce89c157b5af6cd0b", size = 1712570, upload-time = "2025-10-28T20:57:22.253Z" }, + { url = "https://files.pythonhosted.org/packages/11/2c/22799d8e720f4697a9e66fd9c02479e40a49de3de2f0bbe7f9f78a987808/aiohttp-3.13.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:e1b4951125ec10c70802f2cb09736c895861cd39fd9dcb35107b4dc8ae6220b8", size = 1733407, upload-time = "2025-10-28T20:57:24.37Z" }, + { url = "https://files.pythonhosted.org/packages/34/cb/90f15dd029f07cebbd91f8238a8b363978b530cd128488085b5703683594/aiohttp-3.13.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:550bf765101ae721ee1d37d8095f47b1f220650f85fe1af37a90ce75bab89d04", size = 1550093, upload-time = "2025-10-28T20:57:26.257Z" }, + { url = "https://files.pythonhosted.org/packages/69/46/12dce9be9d3303ecbf4d30ad45a7683dc63d90733c2d9fe512be6716cd40/aiohttp-3.13.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:fe91b87fc295973096251e2d25a811388e7d8adf3bd2b97ef6ae78bc4ac6c476", size = 1758084, upload-time = "2025-10-28T20:57:28.349Z" }, + { url = "https://files.pythonhosted.org/packages/f9/c8/0932b558da0c302ffd639fc6362a313b98fdf235dc417bc2493da8394df7/aiohttp-3.13.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e0c8e31cfcc4592cb200160344b2fb6ae0f9e4effe06c644b5a125d4ae5ebe23", size = 1716987, upload-time = "2025-10-28T20:57:30.233Z" }, + { url = "https://files.pythonhosted.org/packages/5d/8b/f5bd1a75003daed099baec373aed678f2e9b34f2ad40d85baa1368556396/aiohttp-3.13.2-cp313-cp313-win32.whl", hash = "sha256:0740f31a60848d6edb296a0df827473eede90c689b8f9f2a4cdde74889eb2254", size = 425859, upload-time = "2025-10-28T20:57:32.105Z" }, + { url = "https://files.pythonhosted.org/packages/5d/28/a8a9fc6957b2cee8902414e41816b5ab5536ecf43c3b1843c10e82c559b2/aiohttp-3.13.2-cp313-cp313-win_amd64.whl", hash = "sha256:a88d13e7ca367394908f8a276b89d04a3652044612b9a408a0bb22a5ed976a1a", size = 452192, upload-time = "2025-10-28T20:57:34.166Z" }, + { url = "https://files.pythonhosted.org/packages/9b/36/e2abae1bd815f01c957cbf7be817b3043304e1c87bad526292a0410fdcf9/aiohttp-3.13.2-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:2475391c29230e063ef53a66669b7b691c9bfc3f1426a0f7bcdf1216bdbac38b", size = 735234, upload-time = "2025-10-28T20:57:36.415Z" }, + { url = "https://files.pythonhosted.org/packages/ca/e3/1ee62dde9b335e4ed41db6bba02613295a0d5b41f74a783c142745a12763/aiohttp-3.13.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:f33c8748abef4d8717bb20e8fb1b3e07c6adacb7fd6beaae971a764cf5f30d61", size = 490733, upload-time = "2025-10-28T20:57:38.205Z" }, + { url = "https://files.pythonhosted.org/packages/1a/aa/7a451b1d6a04e8d15a362af3e9b897de71d86feac3babf8894545d08d537/aiohttp-3.13.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ae32f24bbfb7dbb485a24b30b1149e2f200be94777232aeadba3eecece4d0aa4", size = 491303, upload-time = "2025-10-28T20:57:40.122Z" }, + { url = "https://files.pythonhosted.org/packages/57/1e/209958dbb9b01174870f6a7538cd1f3f28274fdbc88a750c238e2c456295/aiohttp-3.13.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d7f02042c1f009ffb70067326ef183a047425bb2ff3bc434ead4dd4a4a66a2b", size = 1717965, upload-time = "2025-10-28T20:57:42.28Z" }, + { url = "https://files.pythonhosted.org/packages/08/aa/6a01848d6432f241416bc4866cae8dc03f05a5a884d2311280f6a09c73d6/aiohttp-3.13.2-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:93655083005d71cd6c072cdab54c886e6570ad2c4592139c3fb967bfc19e4694", size = 1667221, upload-time = "2025-10-28T20:57:44.869Z" }, + { url = "https://files.pythonhosted.org/packages/87/4f/36c1992432d31bbc789fa0b93c768d2e9047ec8c7177e5cd84ea85155f36/aiohttp-3.13.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0db1e24b852f5f664cd728db140cf11ea0e82450471232a394b3d1a540b0f906", size = 1757178, upload-time = "2025-10-28T20:57:47.216Z" }, + { url = "https://files.pythonhosted.org/packages/ac/b4/8e940dfb03b7e0f68a82b88fd182b9be0a65cb3f35612fe38c038c3112cf/aiohttp-3.13.2-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b009194665bcd128e23eaddef362e745601afa4641930848af4c8559e88f18f9", size = 1838001, upload-time = "2025-10-28T20:57:49.337Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ef/39f3448795499c440ab66084a9db7d20ca7662e94305f175a80f5b7e0072/aiohttp-3.13.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c038a8fdc8103cd51dbd986ecdce141473ffd9775a7a8057a6ed9c3653478011", size = 1716325, upload-time = "2025-10-28T20:57:51.327Z" }, + { url = "https://files.pythonhosted.org/packages/d7/51/b311500ffc860b181c05d91c59a1313bdd05c82960fdd4035a15740d431e/aiohttp-3.13.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:66bac29b95a00db411cd758fea0e4b9bdba6d549dfe333f9a945430f5f2cc5a6", size = 1547978, upload-time = "2025-10-28T20:57:53.554Z" }, + { url = "https://files.pythonhosted.org/packages/31/64/b9d733296ef79815226dab8c586ff9e3df41c6aff2e16c06697b2d2e6775/aiohttp-3.13.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4ebf9cfc9ba24a74cf0718f04aac2a3bbe745902cc7c5ebc55c0f3b5777ef213", size = 1682042, upload-time = "2025-10-28T20:57:55.617Z" }, + { url = "https://files.pythonhosted.org/packages/3f/30/43d3e0f9d6473a6db7d472104c4eff4417b1e9df01774cb930338806d36b/aiohttp-3.13.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a4b88ebe35ce54205c7074f7302bd08a4cb83256a3e0870c72d6f68a3aaf8e49", size = 1680085, upload-time = "2025-10-28T20:57:57.59Z" }, + { url = "https://files.pythonhosted.org/packages/16/51/c709f352c911b1864cfd1087577760ced64b3e5bee2aa88b8c0c8e2e4972/aiohttp-3.13.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:98c4fb90bb82b70a4ed79ca35f656f4281885be076f3f970ce315402b53099ae", size = 1728238, upload-time = "2025-10-28T20:57:59.525Z" }, + { url = "https://files.pythonhosted.org/packages/19/e2/19bd4c547092b773caeb48ff5ae4b1ae86756a0ee76c16727fcfd281404b/aiohttp-3.13.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:ec7534e63ae0f3759df3a1ed4fa6bc8f75082a924b590619c0dd2f76d7043caa", size = 1544395, upload-time = "2025-10-28T20:58:01.914Z" }, + { url = "https://files.pythonhosted.org/packages/cf/87/860f2803b27dfc5ed7be532832a3498e4919da61299b4a1f8eb89b8ff44d/aiohttp-3.13.2-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:5b927cf9b935a13e33644cbed6c8c4b2d0f25b713d838743f8fe7191b33829c4", size = 1742965, upload-time = "2025-10-28T20:58:03.972Z" }, + { url = "https://files.pythonhosted.org/packages/67/7f/db2fc7618925e8c7a601094d5cbe539f732df4fb570740be88ed9e40e99a/aiohttp-3.13.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:88d6c017966a78c5265d996c19cdb79235be5e6412268d7e2ce7dee339471b7a", size = 1697585, upload-time = "2025-10-28T20:58:06.189Z" }, + { url = "https://files.pythonhosted.org/packages/0c/07/9127916cb09bb38284db5036036042b7b2c514c8ebaeee79da550c43a6d6/aiohttp-3.13.2-cp314-cp314-win32.whl", hash = "sha256:f7c183e786e299b5d6c49fb43a769f8eb8e04a2726a2bd5887b98b5cc2d67940", size = 431621, upload-time = "2025-10-28T20:58:08.636Z" }, + { url = "https://files.pythonhosted.org/packages/fb/41/554a8a380df6d3a2bba8a7726429a23f4ac62aaf38de43bb6d6cde7b4d4d/aiohttp-3.13.2-cp314-cp314-win_amd64.whl", hash = "sha256:fe242cd381e0fb65758faf5ad96c2e460df6ee5b2de1072fe97e4127927e00b4", size = 457627, upload-time = "2025-10-28T20:58:11Z" }, + { url = "https://files.pythonhosted.org/packages/c7/8e/3824ef98c039d3951cb65b9205a96dd2b20f22241ee17d89c5701557c826/aiohttp-3.13.2-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:f10d9c0b0188fe85398c61147bbd2a657d616c876863bfeff43376e0e3134673", size = 767360, upload-time = "2025-10-28T20:58:13.358Z" }, + { url = "https://files.pythonhosted.org/packages/a4/0f/6a03e3fc7595421274fa34122c973bde2d89344f8a881b728fa8c774e4f1/aiohttp-3.13.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:e7c952aefdf2460f4ae55c5e9c3e80aa72f706a6317e06020f80e96253b1accd", size = 504616, upload-time = "2025-10-28T20:58:15.339Z" }, + { url = "https://files.pythonhosted.org/packages/c6/aa/ed341b670f1bc8a6f2c6a718353d13b9546e2cef3544f573c6a1ff0da711/aiohttp-3.13.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c20423ce14771d98353d2e25e83591fa75dfa90a3c1848f3d7c68243b4fbded3", size = 509131, upload-time = "2025-10-28T20:58:17.693Z" }, + { url = "https://files.pythonhosted.org/packages/7f/f0/c68dac234189dae5c4bbccc0f96ce0cc16b76632cfc3a08fff180045cfa4/aiohttp-3.13.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e96eb1a34396e9430c19d8338d2ec33015e4a87ef2b4449db94c22412e25ccdf", size = 1864168, upload-time = "2025-10-28T20:58:20.113Z" }, + { url = "https://files.pythonhosted.org/packages/8f/65/75a9a76db8364b5d0e52a0c20eabc5d52297385d9af9c35335b924fafdee/aiohttp-3.13.2-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:23fb0783bc1a33640036465019d3bba069942616a6a2353c6907d7fe1ccdaf4e", size = 1719200, upload-time = "2025-10-28T20:58:22.583Z" }, + { url = "https://files.pythonhosted.org/packages/f5/55/8df2ed78d7f41d232f6bd3ff866b6f617026551aa1d07e2f03458f964575/aiohttp-3.13.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2e1a9bea6244a1d05a4e57c295d69e159a5c50d8ef16aa390948ee873478d9a5", size = 1843497, upload-time = "2025-10-28T20:58:24.672Z" }, + { url = "https://files.pythonhosted.org/packages/e9/e0/94d7215e405c5a02ccb6a35c7a3a6cfff242f457a00196496935f700cde5/aiohttp-3.13.2-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0a3d54e822688b56e9f6b5816fb3de3a3a64660efac64e4c2dc435230ad23bad", size = 1935703, upload-time = "2025-10-28T20:58:26.758Z" }, + { url = "https://files.pythonhosted.org/packages/0b/78/1eeb63c3f9b2d1015a4c02788fb543141aad0a03ae3f7a7b669b2483f8d4/aiohttp-3.13.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7a653d872afe9f33497215745da7a943d1dc15b728a9c8da1c3ac423af35178e", size = 1792738, upload-time = "2025-10-28T20:58:29.787Z" }, + { url = "https://files.pythonhosted.org/packages/41/75/aaf1eea4c188e51538c04cc568040e3082db263a57086ea74a7d38c39e42/aiohttp-3.13.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:56d36e80d2003fa3fc0207fac644216d8532e9504a785ef9a8fd013f84a42c61", size = 1624061, upload-time = "2025-10-28T20:58:32.529Z" }, + { url = "https://files.pythonhosted.org/packages/9b/c2/3b6034de81fbcc43de8aeb209073a2286dfb50b86e927b4efd81cf848197/aiohttp-3.13.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:78cd586d8331fb8e241c2dd6b2f4061778cc69e150514b39a9e28dd050475661", size = 1789201, upload-time = "2025-10-28T20:58:34.618Z" }, + { url = "https://files.pythonhosted.org/packages/c9/38/c15dcf6d4d890217dae79d7213988f4e5fe6183d43893a9cf2fe9e84ca8d/aiohttp-3.13.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:20b10bbfbff766294fe99987f7bb3b74fdd2f1a2905f2562132641ad434dcf98", size = 1776868, upload-time = "2025-10-28T20:58:38.835Z" }, + { url = "https://files.pythonhosted.org/packages/04/75/f74fd178ac81adf4f283a74847807ade5150e48feda6aef024403716c30c/aiohttp-3.13.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:9ec49dff7e2b3c85cdeaa412e9d438f0ecd71676fde61ec57027dd392f00c693", size = 1790660, upload-time = "2025-10-28T20:58:41.507Z" }, + { url = "https://files.pythonhosted.org/packages/e7/80/7368bd0d06b16b3aba358c16b919e9c46cf11587dc572091031b0e9e3ef0/aiohttp-3.13.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:94f05348c4406450f9d73d38efb41d669ad6cd90c7ee194810d0eefbfa875a7a", size = 1617548, upload-time = "2025-10-28T20:58:43.674Z" }, + { url = "https://files.pythonhosted.org/packages/7d/4b/a6212790c50483cb3212e507378fbe26b5086d73941e1ec4b56a30439688/aiohttp-3.13.2-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:fa4dcb605c6f82a80c7f95713c2b11c3b8e9893b3ebd2bc9bde93165ed6107be", size = 1817240, upload-time = "2025-10-28T20:58:45.787Z" }, + { url = "https://files.pythonhosted.org/packages/ff/f7/ba5f0ba4ea8d8f3c32850912944532b933acbf0f3a75546b89269b9b7dde/aiohttp-3.13.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cf00e5db968c3f67eccd2778574cf64d8b27d95b237770aa32400bd7a1ca4f6c", size = 1762334, upload-time = "2025-10-28T20:58:47.936Z" }, + { url = "https://files.pythonhosted.org/packages/7e/83/1a5a1856574588b1cad63609ea9ad75b32a8353ac995d830bf5da9357364/aiohttp-3.13.2-cp314-cp314t-win32.whl", hash = "sha256:d23b5fe492b0805a50d3371e8a728a9134d8de5447dce4c885f5587294750734", size = 464685, upload-time = "2025-10-28T20:58:50.642Z" }, + { url = "https://files.pythonhosted.org/packages/9f/4d/d22668674122c08f4d56972297c51a624e64b3ed1efaa40187607a7cb66e/aiohttp-3.13.2-cp314-cp314t-win_amd64.whl", hash = "sha256:ff0a7b0a82a7ab905cbda74006318d1b12e37c797eb1b0d4eb3e316cf47f658f", size = 498093, upload-time = "2025-10-28T20:58:52.782Z" }, + { url = "https://files.pythonhosted.org/packages/04/4a/3da532fdf51b5e58fffa1a86d6569184cb1bf4bf81cd4434b6541a8d14fd/aiohttp-3.13.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7fbdf5ad6084f1940ce88933de34b62358d0f4a0b6ec097362dcd3e5a65a4989", size = 739009, upload-time = "2025-10-28T20:58:55.682Z" }, + { url = "https://files.pythonhosted.org/packages/89/74/fefa6f7939cdc1d77e5cad712004e675a8847dccc589dcc3abca7feaed73/aiohttp-3.13.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7c3a50345635a02db61792c85bb86daffac05330f6473d524f1a4e3ef9d0046d", size = 495308, upload-time = "2025-10-28T20:58:58.408Z" }, + { url = "https://files.pythonhosted.org/packages/4e/b4/a0638ae1f12d09a0dc558870968a2f19a1eba1b10ad0a85ef142ddb40b50/aiohttp-3.13.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0e87dff73f46e969af38ab3f7cb75316a7c944e2e574ff7c933bc01b10def7f5", size = 490624, upload-time = "2025-10-28T20:59:00.479Z" }, + { url = "https://files.pythonhosted.org/packages/02/73/361cd4cac9d98a5a4183d1f26faf7b777330f8dba838c5aae2412862bdd0/aiohttp-3.13.2-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2adebd4577724dcae085665f294cc57c8701ddd4d26140504db622b8d566d7aa", size = 1662968, upload-time = "2025-10-28T20:59:03.105Z" }, + { url = "https://files.pythonhosted.org/packages/9e/93/ce2ca7584555a6c7dd78f2e6b539a96c5172d88815e13a05a576e14a5a22/aiohttp-3.13.2-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e036a3a645fe92309ec34b918394bb377950cbb43039a97edae6c08db64b23e2", size = 1627117, upload-time = "2025-10-28T20:59:05.274Z" }, + { url = "https://files.pythonhosted.org/packages/a6/42/7ee0e699111f5fc20a69b3203e8f5d5da0b681f270b90bc088d15e339980/aiohttp-3.13.2-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:23ad365e30108c422d0b4428cf271156dd56790f6dd50d770b8e360e6c5ab2e6", size = 1724037, upload-time = "2025-10-28T20:59:07.522Z" }, + { url = "https://files.pythonhosted.org/packages/66/88/67ad5ff11dd61dd1d7882cda39f085d5fca31cf7e2143f5173429d8a591e/aiohttp-3.13.2-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1f9b2c2d4b9d958b1f9ae0c984ec1dd6b6689e15c75045be8ccb4011426268ca", size = 1812899, upload-time = "2025-10-28T20:59:11.698Z" }, + { url = "https://files.pythonhosted.org/packages/60/1b/a46f6e1c2a347b9c7a789292279c159b327fadecbf8340f3b05fffff1151/aiohttp-3.13.2-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3a92cf4b9bea33e15ecbaa5c59921be0f23222608143d025c989924f7e3e0c07", size = 1660961, upload-time = "2025-10-28T20:59:14.425Z" }, + { url = "https://files.pythonhosted.org/packages/44/cc/1af9e466eafd9b5d8922238c69aaf95b656137add4c5db65f63ee129bf3c/aiohttp-3.13.2-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:070599407f4954021509193404c4ac53153525a19531051661440644728ba9a7", size = 1553851, upload-time = "2025-10-28T20:59:17.044Z" }, + { url = "https://files.pythonhosted.org/packages/e5/d1/9e5f4f40f9d0ee5668e9b5e7ebfb0eaf371cc09da03785decdc5da56f4b3/aiohttp-3.13.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:29562998ec66f988d49fb83c9b01694fa927186b781463f376c5845c121e4e0b", size = 1634260, upload-time = "2025-10-28T20:59:19.378Z" }, + { url = "https://files.pythonhosted.org/packages/83/2e/5d065091c4ae8b55a153f458f19308191bad3b62a89496aa081385486338/aiohttp-3.13.2-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:4dd3db9d0f4ebca1d887d76f7cdbcd1116ac0d05a9221b9dad82c64a62578c4d", size = 1639499, upload-time = "2025-10-28T20:59:22.013Z" }, + { url = "https://files.pythonhosted.org/packages/a3/de/58ae6dc73691a51ff16f69a94d13657bf417456fa0fdfed2b59dd6b4c293/aiohttp-3.13.2-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:d7bc4b7f9c4921eba72677cd9fedd2308f4a4ca3e12fab58935295ad9ea98700", size = 1694087, upload-time = "2025-10-28T20:59:24.773Z" }, + { url = "https://files.pythonhosted.org/packages/45/fe/4d9df516268867d83041b6c073ee15cd532dbea58b82d675a7e1cf2ec24c/aiohttp-3.13.2-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:dacd50501cd017f8cccb328da0c90823511d70d24a323196826d923aad865901", size = 1540532, upload-time = "2025-10-28T20:59:27.982Z" }, + { url = "https://files.pythonhosted.org/packages/24/e7/a802619308232499482bf30b3530efb5d141481cfd61850368350fb1acb5/aiohttp-3.13.2-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:8b2f1414f6a1e0683f212ec80e813f4abef94c739fd090b66c9adf9d2a05feac", size = 1710369, upload-time = "2025-10-28T20:59:30.363Z" }, + { url = "https://files.pythonhosted.org/packages/62/08/e8593f39f025efe96ef59550d17cf097222d84f6f84798bedac5bf037fce/aiohttp-3.13.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:04c3971421576ed24c191f610052bcb2f059e395bc2489dd99e397f9bc466329", size = 1649296, upload-time = "2025-10-28T20:59:33.285Z" }, + { url = "https://files.pythonhosted.org/packages/e5/fd/ffbc1b6aa46fc6c284af4a438b2c7eab79af1c8ac4b6d2ced185c17f403e/aiohttp-3.13.2-cp39-cp39-win32.whl", hash = "sha256:9f377d0a924e5cc94dc620bc6366fc3e889586a7f18b748901cf016c916e2084", size = 432980, upload-time = "2025-10-28T20:59:35.515Z" }, + { url = "https://files.pythonhosted.org/packages/ad/a9/d47e7873175a4d8aed425f2cdea2df700b2dd44fac024ffbd83455a69a50/aiohttp-3.13.2-cp39-cp39-win_amd64.whl", hash = "sha256:9c705601e16c03466cb72011bd1af55d68fa65b045356d8f96c216e5f6db0fa5", size = 456021, upload-time = "2025-10-28T20:59:37.659Z" }, +] + +[[package]] +name = "aiosignal" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "frozenlist" }, + { name = "typing-extensions", marker = "python_full_version < '3.13' or (extra == 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/61/62/06741b579156360248d1ec624842ad0edf697050bbaf7c3e46394e106ad1/aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7", size = 25007, upload-time = "2025-07-03T22:54:43.528Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e", size = 7490, upload-time = "2025-07-03T22:54:42.156Z" }, +] + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] +name = "anyio" +version = "4.12.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "exceptiongroup", marker = "python_full_version < '3.11' or (extra == 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2')" }, + { name = "idna" }, + { name = "typing-extensions", marker = "python_full_version < '3.13' or (extra == 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/16/ce/8a777047513153587e5434fd752e89334ac33e379aa3497db860eeb60377/anyio-4.12.0.tar.gz", hash = "sha256:73c693b567b0c55130c104d0b43a9baf3aa6a31fc6110116509f27bf75e21ec0", size = 228266, upload-time = "2025-11-28T23:37:38.911Z" } +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 = "async-timeout" +version = "5.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a5/ae/136395dfbfe00dfc94da3f3e136d0b13f394cba8f4841120e34226265780/async_timeout-5.0.1.tar.gz", hash = "sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3", size = 9274, upload-time = "2024-11-06T16:41:39.6Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/ba/e2081de779ca30d473f21f5b30e0e737c438205440784c7dfc81efc2b029/async_timeout-5.0.1-py3-none-any.whl", hash = "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c", size = 6233, upload-time = "2024-11-06T16:41:37.9Z" }, +] + +[[package]] +name = "attrs" +version = "25.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6b/5c/685e6633917e101e5dcb62b9dd76946cbb57c26e133bae9e0cd36033c0a9/attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11", size = 934251, upload-time = "2025-10-06T13:54:44.725Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615, upload-time = "2025-10-06T13:54:43.17Z" }, +] + +[[package]] +name = "backports-asyncio-runner" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8e/ff/70dca7d7cb1cbc0edb2c6cc0c38b65cba36cccc491eca64cabd5fe7f8670/backports_asyncio_runner-1.2.0.tar.gz", hash = "sha256:a5aa7b2b7d8f8bfcaa2b57313f70792df84e32a2a746f585213373f900b42162", size = 69893, upload-time = "2025-07-02T02:27:15.685Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/59/76ab57e3fe74484f48a53f8e337171b4a2349e506eabe136d7e01d059086/backports_asyncio_runner-1.2.0-py3-none-any.whl", hash = "sha256:0da0a936a8aeb554eccb426dc55af3ba63bcdc69fa1a600b5bb305413a4477b5", size = 12313, upload-time = "2025-07-02T02:27:14.263Z" }, +] + +[[package]] +name = "certifi" +version = "2025.11.12" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/8c/58f469717fa48465e4a50c014a0400602d3c437d7c0c468e17ada824da3a/certifi-2025.11.12.tar.gz", hash = "sha256:d8ab5478f2ecd78af242878415affce761ca6bc54a22a27e026d7c25357c3316", size = 160538, upload-time = "2025-11-12T02:54:51.517Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/70/7d/9bc192684cea499815ff478dfcdc13835ddf401365057044fb721ec6bddb/certifi-2025.11.12-py3-none-any.whl", hash = "sha256:97de8790030bbd5c2d96b7ec782fc2f7820ef8dba6db909ccf95449f2d062d4b", size = 159438, upload-time = "2025-11-12T02:54:49.735Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +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 = "dirty-equals" +version = "0.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/30/1d/c5913ac9d6615515a00f4bdc71356d302437cb74ff2e9aaccd3c14493b78/dirty_equals-0.11.tar.gz", hash = "sha256:f4ac74ee88f2d11e2fa0f65eb30ee4f07105c5f86f4dc92b09eb1138775027c3", size = 128067, upload-time = "2025-11-17T01:51:24.451Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bb/8d/dbff05239043271dbeace563a7686212a3dd517864a35623fe4d4a64ca19/dirty_equals-0.11-py3-none-any.whl", hash = "sha256:b1d7093273fc2f9be12f443a8ead954ef6daaf6746fd42ef3a5616433ee85286", size = 28051, upload-time = "2025-11-17T01:51:22.849Z" }, +] + +[[package]] +name = "distro" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fc/f8/98eea607f65de6527f8a2e8885fc8015d3e6f5775df186e443e0964a11c3/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", size = 60722, upload-time = "2023-12-24T09:54:32.31Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277, upload-time = "2023-12-24T09:54:30.421Z" }, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.13' or (extra == 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2')" }, +] +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 = [ + { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, +] + +[[package]] +name = "execnet" +version = "2.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bf/89/780e11f9588d9e7128a3f87788354c7946a9cbb1401ad38a48c4db9a4f07/execnet-2.1.2.tar.gz", hash = "sha256:63d83bfdd9a23e35b9c6a3261412324f964c2ec8dcd8d3c6916ee9373e0befcd", size = 166622, upload-time = "2025-11-12T09:56:37.75Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl", hash = "sha256:67fba928dd5a544b783f6056f449e5e3931a5c378b128bc18501f7ea79e296ec", size = 40708, upload-time = "2025-11-12T09:56:36.333Z" }, +] + +[[package]] +name = "frozenlist" +version = "1.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2d/f5/c831fac6cc817d26fd54c7eaccd04ef7e0288806943f7cc5bbf69f3ac1f0/frozenlist-1.8.0.tar.gz", hash = "sha256:3ede829ed8d842f6cd48fc7081d7a41001a56f1f38603f9d49bf3020d59a31ad", size = 45875, upload-time = "2025-10-06T05:38:17.865Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/4a/557715d5047da48d54e659203b9335be7bfaafda2c3f627b7c47e0b3aaf3/frozenlist-1.8.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b37f6d31b3dcea7deb5e9696e529a6aa4a898adc33db82da12e4c60a7c4d2011", size = 86230, upload-time = "2025-10-06T05:35:23.699Z" }, + { url = "https://files.pythonhosted.org/packages/a2/fb/c85f9fed3ea8fe8740e5b46a59cc141c23b842eca617da8876cfce5f760e/frozenlist-1.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ef2b7b394f208233e471abc541cc6991f907ffd47dc72584acee3147899d6565", size = 49621, upload-time = "2025-10-06T05:35:25.341Z" }, + { url = "https://files.pythonhosted.org/packages/63/70/26ca3f06aace16f2352796b08704338d74b6d1a24ca38f2771afbb7ed915/frozenlist-1.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a88f062f072d1589b7b46e951698950e7da00442fc1cacbe17e19e025dc327ad", size = 49889, upload-time = "2025-10-06T05:35:26.797Z" }, + { url = "https://files.pythonhosted.org/packages/5d/ed/c7895fd2fde7f3ee70d248175f9b6cdf792fb741ab92dc59cd9ef3bd241b/frozenlist-1.8.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f57fb59d9f385710aa7060e89410aeb5058b99e62f4d16b08b91986b9a2140c2", size = 219464, upload-time = "2025-10-06T05:35:28.254Z" }, + { url = "https://files.pythonhosted.org/packages/6b/83/4d587dccbfca74cb8b810472392ad62bfa100bf8108c7223eb4c4fa2f7b3/frozenlist-1.8.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:799345ab092bee59f01a915620b5d014698547afd011e691a208637312db9186", size = 221649, upload-time = "2025-10-06T05:35:29.454Z" }, + { url = "https://files.pythonhosted.org/packages/6a/c6/fd3b9cd046ec5fff9dab66831083bc2077006a874a2d3d9247dea93ddf7e/frozenlist-1.8.0-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c23c3ff005322a6e16f71bf8692fcf4d5a304aaafe1e262c98c6d4adc7be863e", size = 219188, upload-time = "2025-10-06T05:35:30.951Z" }, + { url = "https://files.pythonhosted.org/packages/ce/80/6693f55eb2e085fc8afb28cf611448fb5b90e98e068fa1d1b8d8e66e5c7d/frozenlist-1.8.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8a76ea0f0b9dfa06f254ee06053d93a600865b3274358ca48a352ce4f0798450", size = 231748, upload-time = "2025-10-06T05:35:32.101Z" }, + { url = "https://files.pythonhosted.org/packages/97/d6/e9459f7c5183854abd989ba384fe0cc1a0fb795a83c033f0571ec5933ca4/frozenlist-1.8.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c7366fe1418a6133d5aa824ee53d406550110984de7637d65a178010f759c6ef", size = 236351, upload-time = "2025-10-06T05:35:33.834Z" }, + { url = "https://files.pythonhosted.org/packages/97/92/24e97474b65c0262e9ecd076e826bfd1d3074adcc165a256e42e7b8a7249/frozenlist-1.8.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:13d23a45c4cebade99340c4165bd90eeb4a56c6d8a9d8aa49568cac19a6d0dc4", size = 218767, upload-time = "2025-10-06T05:35:35.205Z" }, + { url = "https://files.pythonhosted.org/packages/ee/bf/dc394a097508f15abff383c5108cb8ad880d1f64a725ed3b90d5c2fbf0bb/frozenlist-1.8.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:e4a3408834f65da56c83528fb52ce7911484f0d1eaf7b761fc66001db1646eff", size = 235887, upload-time = "2025-10-06T05:35:36.354Z" }, + { url = "https://files.pythonhosted.org/packages/40/90/25b201b9c015dbc999a5baf475a257010471a1fa8c200c843fd4abbee725/frozenlist-1.8.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:42145cd2748ca39f32801dad54aeea10039da6f86e303659db90db1c4b614c8c", size = 228785, upload-time = "2025-10-06T05:35:37.949Z" }, + { url = "https://files.pythonhosted.org/packages/84/f4/b5bc148df03082f05d2dd30c089e269acdbe251ac9a9cf4e727b2dbb8a3d/frozenlist-1.8.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:e2de870d16a7a53901e41b64ffdf26f2fbb8917b3e6ebf398098d72c5b20bd7f", size = 230312, upload-time = "2025-10-06T05:35:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/db/4b/87e95b5d15097c302430e647136b7d7ab2398a702390cf4c8601975709e7/frozenlist-1.8.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:20e63c9493d33ee48536600d1a5c95eefc870cd71e7ab037763d1fbb89cc51e7", size = 217650, upload-time = "2025-10-06T05:35:40.377Z" }, + { url = "https://files.pythonhosted.org/packages/e5/70/78a0315d1fea97120591a83e0acd644da638c872f142fd72a6cebee825f3/frozenlist-1.8.0-cp310-cp310-win32.whl", hash = "sha256:adbeebaebae3526afc3c96fad434367cafbfd1b25d72369a9e5858453b1bb71a", size = 39659, upload-time = "2025-10-06T05:35:41.863Z" }, + { url = "https://files.pythonhosted.org/packages/66/aa/3f04523fb189a00e147e60c5b2205126118f216b0aa908035c45336e27e4/frozenlist-1.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:667c3777ca571e5dbeb76f331562ff98b957431df140b54c85fd4d52eea8d8f6", size = 43837, upload-time = "2025-10-06T05:35:43.205Z" }, + { url = "https://files.pythonhosted.org/packages/39/75/1135feecdd7c336938bd55b4dc3b0dfc46d85b9be12ef2628574b28de776/frozenlist-1.8.0-cp310-cp310-win_arm64.whl", hash = "sha256:80f85f0a7cc86e7a54c46d99c9e1318ff01f4687c172ede30fd52d19d1da1c8e", size = 39989, upload-time = "2025-10-06T05:35:44.596Z" }, + { url = "https://files.pythonhosted.org/packages/bc/03/077f869d540370db12165c0aa51640a873fb661d8b315d1d4d67b284d7ac/frozenlist-1.8.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:09474e9831bc2b2199fad6da3c14c7b0fbdd377cce9d3d77131be28906cb7d84", size = 86912, upload-time = "2025-10-06T05:35:45.98Z" }, + { url = "https://files.pythonhosted.org/packages/df/b5/7610b6bd13e4ae77b96ba85abea1c8cb249683217ef09ac9e0ae93f25a91/frozenlist-1.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:17c883ab0ab67200b5f964d2b9ed6b00971917d5d8a92df149dc2c9779208ee9", size = 50046, upload-time = "2025-10-06T05:35:47.009Z" }, + { url = "https://files.pythonhosted.org/packages/6e/ef/0e8f1fe32f8a53dd26bdd1f9347efe0778b0fddf62789ea683f4cc7d787d/frozenlist-1.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fa47e444b8ba08fffd1c18e8cdb9a75db1b6a27f17507522834ad13ed5922b93", size = 50119, upload-time = "2025-10-06T05:35:48.38Z" }, + { url = "https://files.pythonhosted.org/packages/11/b1/71a477adc7c36e5fb628245dfbdea2166feae310757dea848d02bd0689fd/frozenlist-1.8.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2552f44204b744fba866e573be4c1f9048d6a324dfe14475103fd51613eb1d1f", size = 231067, upload-time = "2025-10-06T05:35:49.97Z" }, + { url = "https://files.pythonhosted.org/packages/45/7e/afe40eca3a2dc19b9904c0f5d7edfe82b5304cb831391edec0ac04af94c2/frozenlist-1.8.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:957e7c38f250991e48a9a73e6423db1bb9dd14e722a10f6b8bb8e16a0f55f695", size = 233160, upload-time = "2025-10-06T05:35:51.729Z" }, + { url = "https://files.pythonhosted.org/packages/a6/aa/7416eac95603ce428679d273255ffc7c998d4132cfae200103f164b108aa/frozenlist-1.8.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8585e3bb2cdea02fc88ffa245069c36555557ad3609e83be0ec71f54fd4abb52", size = 228544, upload-time = "2025-10-06T05:35:53.246Z" }, + { url = "https://files.pythonhosted.org/packages/8b/3d/2a2d1f683d55ac7e3875e4263d28410063e738384d3adc294f5ff3d7105e/frozenlist-1.8.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:edee74874ce20a373d62dc28b0b18b93f645633c2943fd90ee9d898550770581", size = 243797, upload-time = "2025-10-06T05:35:54.497Z" }, + { url = "https://files.pythonhosted.org/packages/78/1e/2d5565b589e580c296d3bb54da08d206e797d941a83a6fdea42af23be79c/frozenlist-1.8.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c9a63152fe95756b85f31186bddf42e4c02c6321207fd6601a1c89ebac4fe567", size = 247923, upload-time = "2025-10-06T05:35:55.861Z" }, + { url = "https://files.pythonhosted.org/packages/aa/c3/65872fcf1d326a7f101ad4d86285c403c87be7d832b7470b77f6d2ed5ddc/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b6db2185db9be0a04fecf2f241c70b63b1a242e2805be291855078f2b404dd6b", size = 230886, upload-time = "2025-10-06T05:35:57.399Z" }, + { url = "https://files.pythonhosted.org/packages/a0/76/ac9ced601d62f6956f03cc794f9e04c81719509f85255abf96e2510f4265/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:f4be2e3d8bc8aabd566f8d5b8ba7ecc09249d74ba3c9ed52e54dc23a293f0b92", size = 245731, upload-time = "2025-10-06T05:35:58.563Z" }, + { url = "https://files.pythonhosted.org/packages/b9/49/ecccb5f2598daf0b4a1415497eba4c33c1e8ce07495eb07d2860c731b8d5/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:c8d1634419f39ea6f5c427ea2f90ca85126b54b50837f31497f3bf38266e853d", size = 241544, upload-time = "2025-10-06T05:35:59.719Z" }, + { url = "https://files.pythonhosted.org/packages/53/4b/ddf24113323c0bbcc54cb38c8b8916f1da7165e07b8e24a717b4a12cbf10/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:1a7fa382a4a223773ed64242dbe1c9c326ec09457e6b8428efb4118c685c3dfd", size = 241806, upload-time = "2025-10-06T05:36:00.959Z" }, + { url = "https://files.pythonhosted.org/packages/a7/fb/9b9a084d73c67175484ba2789a59f8eebebd0827d186a8102005ce41e1ba/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:11847b53d722050808926e785df837353bd4d75f1d494377e59b23594d834967", size = 229382, upload-time = "2025-10-06T05:36:02.22Z" }, + { url = "https://files.pythonhosted.org/packages/95/a3/c8fb25aac55bf5e12dae5c5aa6a98f85d436c1dc658f21c3ac73f9fa95e5/frozenlist-1.8.0-cp311-cp311-win32.whl", hash = "sha256:27c6e8077956cf73eadd514be8fb04d77fc946a7fe9f7fe167648b0b9085cc25", size = 39647, upload-time = "2025-10-06T05:36:03.409Z" }, + { url = "https://files.pythonhosted.org/packages/0a/f5/603d0d6a02cfd4c8f2a095a54672b3cf967ad688a60fb9faf04fc4887f65/frozenlist-1.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:ac913f8403b36a2c8610bbfd25b8013488533e71e62b4b4adce9c86c8cea905b", size = 44064, upload-time = "2025-10-06T05:36:04.368Z" }, + { url = "https://files.pythonhosted.org/packages/5d/16/c2c9ab44e181f043a86f9a8f84d5124b62dbcb3a02c0977ec72b9ac1d3e0/frozenlist-1.8.0-cp311-cp311-win_arm64.whl", hash = "sha256:d4d3214a0f8394edfa3e303136d0575eece0745ff2b47bd2cb2e66dd92d4351a", size = 39937, upload-time = "2025-10-06T05:36:05.669Z" }, + { url = "https://files.pythonhosted.org/packages/69/29/948b9aa87e75820a38650af445d2ef2b6b8a6fab1a23b6bb9e4ef0be2d59/frozenlist-1.8.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:78f7b9e5d6f2fdb88cdde9440dc147259b62b9d3b019924def9f6478be254ac1", size = 87782, upload-time = "2025-10-06T05:36:06.649Z" }, + { url = "https://files.pythonhosted.org/packages/64/80/4f6e318ee2a7c0750ed724fa33a4bdf1eacdc5a39a7a24e818a773cd91af/frozenlist-1.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:229bf37d2e4acdaf808fd3f06e854a4a7a3661e871b10dc1f8f1896a3b05f18b", size = 50594, upload-time = "2025-10-06T05:36:07.69Z" }, + { url = "https://files.pythonhosted.org/packages/2b/94/5c8a2b50a496b11dd519f4a24cb5496cf125681dd99e94c604ccdea9419a/frozenlist-1.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f833670942247a14eafbb675458b4e61c82e002a148f49e68257b79296e865c4", size = 50448, upload-time = "2025-10-06T05:36:08.78Z" }, + { url = "https://files.pythonhosted.org/packages/6a/bd/d91c5e39f490a49df14320f4e8c80161cfcce09f1e2cde1edd16a551abb3/frozenlist-1.8.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:494a5952b1c597ba44e0e78113a7266e656b9794eec897b19ead706bd7074383", size = 242411, upload-time = "2025-10-06T05:36:09.801Z" }, + { url = "https://files.pythonhosted.org/packages/8f/83/f61505a05109ef3293dfb1ff594d13d64a2324ac3482be2cedc2be818256/frozenlist-1.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96f423a119f4777a4a056b66ce11527366a8bb92f54e541ade21f2374433f6d4", size = 243014, upload-time = "2025-10-06T05:36:11.394Z" }, + { url = "https://files.pythonhosted.org/packages/d8/cb/cb6c7b0f7d4023ddda30cf56b8b17494eb3a79e3fda666bf735f63118b35/frozenlist-1.8.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3462dd9475af2025c31cc61be6652dfa25cbfb56cbbf52f4ccfe029f38decaf8", size = 234909, upload-time = "2025-10-06T05:36:12.598Z" }, + { url = "https://files.pythonhosted.org/packages/31/c5/cd7a1f3b8b34af009fb17d4123c5a778b44ae2804e3ad6b86204255f9ec5/frozenlist-1.8.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4c800524c9cd9bac5166cd6f55285957fcfc907db323e193f2afcd4d9abd69b", size = 250049, upload-time = "2025-10-06T05:36:14.065Z" }, + { url = "https://files.pythonhosted.org/packages/c0/01/2f95d3b416c584a1e7f0e1d6d31998c4a795f7544069ee2e0962a4b60740/frozenlist-1.8.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d6a5df73acd3399d893dafc71663ad22534b5aa4f94e8a2fabfe856c3c1b6a52", size = 256485, upload-time = "2025-10-06T05:36:15.39Z" }, + { url = "https://files.pythonhosted.org/packages/ce/03/024bf7720b3abaebcff6d0793d73c154237b85bdf67b7ed55e5e9596dc9a/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:405e8fe955c2280ce66428b3ca55e12b3c4e9c336fb2103a4937e891c69a4a29", size = 237619, upload-time = "2025-10-06T05:36:16.558Z" }, + { url = "https://files.pythonhosted.org/packages/69/fa/f8abdfe7d76b731f5d8bd217827cf6764d4f1d9763407e42717b4bed50a0/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:908bd3f6439f2fef9e85031b59fd4f1297af54415fb60e4254a95f75b3cab3f3", size = 250320, upload-time = "2025-10-06T05:36:17.821Z" }, + { url = "https://files.pythonhosted.org/packages/f5/3c/b051329f718b463b22613e269ad72138cc256c540f78a6de89452803a47d/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:294e487f9ec720bd8ffcebc99d575f7eff3568a08a253d1ee1a0378754b74143", size = 246820, upload-time = "2025-10-06T05:36:19.046Z" }, + { url = "https://files.pythonhosted.org/packages/0f/ae/58282e8f98e444b3f4dd42448ff36fa38bef29e40d40f330b22e7108f565/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:74c51543498289c0c43656701be6b077f4b265868fa7f8a8859c197006efb608", size = 250518, upload-time = "2025-10-06T05:36:20.763Z" }, + { url = "https://files.pythonhosted.org/packages/8f/96/007e5944694d66123183845a106547a15944fbbb7154788cbf7272789536/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:776f352e8329135506a1d6bf16ac3f87bc25b28e765949282dcc627af36123aa", size = 239096, upload-time = "2025-10-06T05:36:22.129Z" }, + { url = "https://files.pythonhosted.org/packages/66/bb/852b9d6db2fa40be96f29c0d1205c306288f0684df8fd26ca1951d461a56/frozenlist-1.8.0-cp312-cp312-win32.whl", hash = "sha256:433403ae80709741ce34038da08511d4a77062aa924baf411ef73d1146e74faf", size = 39985, upload-time = "2025-10-06T05:36:23.661Z" }, + { url = "https://files.pythonhosted.org/packages/b8/af/38e51a553dd66eb064cdf193841f16f077585d4d28394c2fa6235cb41765/frozenlist-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:34187385b08f866104f0c0617404c8eb08165ab1272e884abc89c112e9c00746", size = 44591, upload-time = "2025-10-06T05:36:24.958Z" }, + { url = "https://files.pythonhosted.org/packages/a7/06/1dc65480ab147339fecc70797e9c2f69d9cea9cf38934ce08df070fdb9cb/frozenlist-1.8.0-cp312-cp312-win_arm64.whl", hash = "sha256:fe3c58d2f5db5fbd18c2987cba06d51b0529f52bc3a6cdc33d3f4eab725104bd", size = 40102, upload-time = "2025-10-06T05:36:26.333Z" }, + { url = "https://files.pythonhosted.org/packages/2d/40/0832c31a37d60f60ed79e9dfb5a92e1e2af4f40a16a29abcc7992af9edff/frozenlist-1.8.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8d92f1a84bb12d9e56f818b3a746f3efba93c1b63c8387a73dde655e1e42282a", size = 85717, upload-time = "2025-10-06T05:36:27.341Z" }, + { url = "https://files.pythonhosted.org/packages/30/ba/b0b3de23f40bc55a7057bd38434e25c34fa48e17f20ee273bbde5e0650f3/frozenlist-1.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:96153e77a591c8adc2ee805756c61f59fef4cf4073a9275ee86fe8cba41241f7", size = 49651, upload-time = "2025-10-06T05:36:28.855Z" }, + { url = "https://files.pythonhosted.org/packages/0c/ab/6e5080ee374f875296c4243c381bbdef97a9ac39c6e3ce1d5f7d42cb78d6/frozenlist-1.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f21f00a91358803399890ab167098c131ec2ddd5f8f5fd5fe9c9f2c6fcd91e40", size = 49417, upload-time = "2025-10-06T05:36:29.877Z" }, + { url = "https://files.pythonhosted.org/packages/d5/4e/e4691508f9477ce67da2015d8c00acd751e6287739123113a9fca6f1604e/frozenlist-1.8.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fb30f9626572a76dfe4293c7194a09fb1fe93ba94c7d4f720dfae3b646b45027", size = 234391, upload-time = "2025-10-06T05:36:31.301Z" }, + { url = "https://files.pythonhosted.org/packages/40/76/c202df58e3acdf12969a7895fd6f3bc016c642e6726aa63bd3025e0fc71c/frozenlist-1.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eaa352d7047a31d87dafcacbabe89df0aa506abb5b1b85a2fb91bc3faa02d822", size = 233048, upload-time = "2025-10-06T05:36:32.531Z" }, + { url = "https://files.pythonhosted.org/packages/f9/c0/8746afb90f17b73ca5979c7a3958116e105ff796e718575175319b5bb4ce/frozenlist-1.8.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:03ae967b4e297f58f8c774c7eabcce57fe3c2434817d4385c50661845a058121", size = 226549, upload-time = "2025-10-06T05:36:33.706Z" }, + { url = "https://files.pythonhosted.org/packages/7e/eb/4c7eefc718ff72f9b6c4893291abaae5fbc0c82226a32dcd8ef4f7a5dbef/frozenlist-1.8.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f6292f1de555ffcc675941d65fffffb0a5bcd992905015f85d0592201793e0e5", size = 239833, upload-time = "2025-10-06T05:36:34.947Z" }, + { url = "https://files.pythonhosted.org/packages/c2/4e/e5c02187cf704224f8b21bee886f3d713ca379535f16893233b9d672ea71/frozenlist-1.8.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29548f9b5b5e3460ce7378144c3010363d8035cea44bc0bf02d57f5a685e084e", size = 245363, upload-time = "2025-10-06T05:36:36.534Z" }, + { url = "https://files.pythonhosted.org/packages/1f/96/cb85ec608464472e82ad37a17f844889c36100eed57bea094518bf270692/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ec3cc8c5d4084591b4237c0a272cc4f50a5b03396a47d9caaf76f5d7b38a4f11", size = 229314, upload-time = "2025-10-06T05:36:38.582Z" }, + { url = "https://files.pythonhosted.org/packages/5d/6f/4ae69c550e4cee66b57887daeebe006fe985917c01d0fff9caab9883f6d0/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:517279f58009d0b1f2e7c1b130b377a349405da3f7621ed6bfae50b10adf20c1", size = 243365, upload-time = "2025-10-06T05:36:40.152Z" }, + { url = "https://files.pythonhosted.org/packages/7a/58/afd56de246cf11780a40a2c28dc7cbabbf06337cc8ddb1c780a2d97e88d8/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:db1e72ede2d0d7ccb213f218df6a078a9c09a7de257c2fe8fcef16d5925230b1", size = 237763, upload-time = "2025-10-06T05:36:41.355Z" }, + { url = "https://files.pythonhosted.org/packages/cb/36/cdfaf6ed42e2644740d4a10452d8e97fa1c062e2a8006e4b09f1b5fd7d63/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b4dec9482a65c54a5044486847b8a66bf10c9cb4926d42927ec4e8fd5db7fed8", size = 240110, upload-time = "2025-10-06T05:36:42.716Z" }, + { url = "https://files.pythonhosted.org/packages/03/a8/9ea226fbefad669f11b52e864c55f0bd57d3c8d7eb07e9f2e9a0b39502e1/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:21900c48ae04d13d416f0e1e0c4d81f7931f73a9dfa0b7a8746fb2fe7dd970ed", size = 233717, upload-time = "2025-10-06T05:36:44.251Z" }, + { url = "https://files.pythonhosted.org/packages/1e/0b/1b5531611e83ba7d13ccc9988967ea1b51186af64c42b7a7af465dcc9568/frozenlist-1.8.0-cp313-cp313-win32.whl", hash = "sha256:8b7b94a067d1c504ee0b16def57ad5738701e4ba10cec90529f13fa03c833496", size = 39628, upload-time = "2025-10-06T05:36:45.423Z" }, + { url = "https://files.pythonhosted.org/packages/d8/cf/174c91dbc9cc49bc7b7aab74d8b734e974d1faa8f191c74af9b7e80848e6/frozenlist-1.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:878be833caa6a3821caf85eb39c5ba92d28e85df26d57afb06b35b2efd937231", size = 43882, upload-time = "2025-10-06T05:36:46.796Z" }, + { url = "https://files.pythonhosted.org/packages/c1/17/502cd212cbfa96eb1388614fe39a3fc9ab87dbbe042b66f97acb57474834/frozenlist-1.8.0-cp313-cp313-win_arm64.whl", hash = "sha256:44389d135b3ff43ba8cc89ff7f51f5a0bb6b63d829c8300f79a2fe4fe61bcc62", size = 39676, upload-time = "2025-10-06T05:36:47.8Z" }, + { url = "https://files.pythonhosted.org/packages/d2/5c/3bbfaa920dfab09e76946a5d2833a7cbdf7b9b4a91c714666ac4855b88b4/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:e25ac20a2ef37e91c1b39938b591457666a0fa835c7783c3a8f33ea42870db94", size = 89235, upload-time = "2025-10-06T05:36:48.78Z" }, + { url = "https://files.pythonhosted.org/packages/d2/d6/f03961ef72166cec1687e84e8925838442b615bd0b8854b54923ce5b7b8a/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:07cdca25a91a4386d2e76ad992916a85038a9b97561bf7a3fd12d5d9ce31870c", size = 50742, upload-time = "2025-10-06T05:36:49.837Z" }, + { url = "https://files.pythonhosted.org/packages/1e/bb/a6d12b7ba4c3337667d0e421f7181c82dda448ce4e7ad7ecd249a16fa806/frozenlist-1.8.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4e0c11f2cc6717e0a741f84a527c52616140741cd812a50422f83dc31749fb52", size = 51725, upload-time = "2025-10-06T05:36:50.851Z" }, + { url = "https://files.pythonhosted.org/packages/bc/71/d1fed0ffe2c2ccd70b43714c6cab0f4188f09f8a67a7914a6b46ee30f274/frozenlist-1.8.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b3210649ee28062ea6099cfda39e147fa1bc039583c8ee4481cb7811e2448c51", size = 284533, upload-time = "2025-10-06T05:36:51.898Z" }, + { url = "https://files.pythonhosted.org/packages/c9/1f/fb1685a7b009d89f9bf78a42d94461bc06581f6e718c39344754a5d9bada/frozenlist-1.8.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:581ef5194c48035a7de2aefc72ac6539823bb71508189e5de01d60c9dcd5fa65", size = 292506, upload-time = "2025-10-06T05:36:53.101Z" }, + { url = "https://files.pythonhosted.org/packages/e6/3b/b991fe1612703f7e0d05c0cf734c1b77aaf7c7d321df4572e8d36e7048c8/frozenlist-1.8.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3ef2d026f16a2b1866e1d86fc4e1291e1ed8a387b2c333809419a2f8b3a77b82", size = 274161, upload-time = "2025-10-06T05:36:54.309Z" }, + { url = "https://files.pythonhosted.org/packages/ca/ec/c5c618767bcdf66e88945ec0157d7f6c4a1322f1473392319b7a2501ded7/frozenlist-1.8.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5500ef82073f599ac84d888e3a8c1f77ac831183244bfd7f11eaa0289fb30714", size = 294676, upload-time = "2025-10-06T05:36:55.566Z" }, + { url = "https://files.pythonhosted.org/packages/7c/ce/3934758637d8f8a88d11f0585d6495ef54b2044ed6ec84492a91fa3b27aa/frozenlist-1.8.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:50066c3997d0091c411a66e710f4e11752251e6d2d73d70d8d5d4c76442a199d", size = 300638, upload-time = "2025-10-06T05:36:56.758Z" }, + { url = "https://files.pythonhosted.org/packages/fc/4f/a7e4d0d467298f42de4b41cbc7ddaf19d3cfeabaf9ff97c20c6c7ee409f9/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5c1c8e78426e59b3f8005e9b19f6ff46e5845895adbde20ece9218319eca6506", size = 283067, upload-time = "2025-10-06T05:36:57.965Z" }, + { url = "https://files.pythonhosted.org/packages/dc/48/c7b163063d55a83772b268e6d1affb960771b0e203b632cfe09522d67ea5/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:eefdba20de0d938cec6a89bd4d70f346a03108a19b9df4248d3cf0d88f1b0f51", size = 292101, upload-time = "2025-10-06T05:36:59.237Z" }, + { url = "https://files.pythonhosted.org/packages/9f/d0/2366d3c4ecdc2fd391e0afa6e11500bfba0ea772764d631bbf82f0136c9d/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:cf253e0e1c3ceb4aaff6df637ce033ff6535fb8c70a764a8f46aafd3d6ab798e", size = 289901, upload-time = "2025-10-06T05:37:00.811Z" }, + { url = "https://files.pythonhosted.org/packages/b8/94/daff920e82c1b70e3618a2ac39fbc01ae3e2ff6124e80739ce5d71c9b920/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:032efa2674356903cd0261c4317a561a6850f3ac864a63fc1583147fb05a79b0", size = 289395, upload-time = "2025-10-06T05:37:02.115Z" }, + { url = "https://files.pythonhosted.org/packages/e3/20/bba307ab4235a09fdcd3cc5508dbabd17c4634a1af4b96e0f69bfe551ebd/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6da155091429aeba16851ecb10a9104a108bcd32f6c1642867eadaee401c1c41", size = 283659, upload-time = "2025-10-06T05:37:03.711Z" }, + { url = "https://files.pythonhosted.org/packages/fd/00/04ca1c3a7a124b6de4f8a9a17cc2fcad138b4608e7a3fc5877804b8715d7/frozenlist-1.8.0-cp313-cp313t-win32.whl", hash = "sha256:0f96534f8bfebc1a394209427d0f8a63d343c9779cda6fc25e8e121b5fd8555b", size = 43492, upload-time = "2025-10-06T05:37:04.915Z" }, + { url = "https://files.pythonhosted.org/packages/59/5e/c69f733a86a94ab10f68e496dc6b7e8bc078ebb415281d5698313e3af3a1/frozenlist-1.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5d63a068f978fc69421fb0e6eb91a9603187527c86b7cd3f534a5b77a592b888", size = 48034, upload-time = "2025-10-06T05:37:06.343Z" }, + { url = "https://files.pythonhosted.org/packages/16/6c/be9d79775d8abe79b05fa6d23da99ad6e7763a1d080fbae7290b286093fd/frozenlist-1.8.0-cp313-cp313t-win_arm64.whl", hash = "sha256:bf0a7e10b077bf5fb9380ad3ae8ce20ef919a6ad93b4552896419ac7e1d8e042", size = 41749, upload-time = "2025-10-06T05:37:07.431Z" }, + { url = "https://files.pythonhosted.org/packages/f1/c8/85da824b7e7b9b6e7f7705b2ecaf9591ba6f79c1177f324c2735e41d36a2/frozenlist-1.8.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cee686f1f4cadeb2136007ddedd0aaf928ab95216e7691c63e50a8ec066336d0", size = 86127, upload-time = "2025-10-06T05:37:08.438Z" }, + { url = "https://files.pythonhosted.org/packages/8e/e8/a1185e236ec66c20afd72399522f142c3724c785789255202d27ae992818/frozenlist-1.8.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:119fb2a1bd47307e899c2fac7f28e85b9a543864df47aa7ec9d3c1b4545f096f", size = 49698, upload-time = "2025-10-06T05:37:09.48Z" }, + { url = "https://files.pythonhosted.org/packages/a1/93/72b1736d68f03fda5fdf0f2180fb6caaae3894f1b854d006ac61ecc727ee/frozenlist-1.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4970ece02dbc8c3a92fcc5228e36a3e933a01a999f7094ff7c23fbd2beeaa67c", size = 49749, upload-time = "2025-10-06T05:37:10.569Z" }, + { url = "https://files.pythonhosted.org/packages/a7/b2/fabede9fafd976b991e9f1b9c8c873ed86f202889b864756f240ce6dd855/frozenlist-1.8.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:cba69cb73723c3f329622e34bdbf5ce1f80c21c290ff04256cff1cd3c2036ed2", size = 231298, upload-time = "2025-10-06T05:37:11.993Z" }, + { url = "https://files.pythonhosted.org/packages/3a/3b/d9b1e0b0eed36e70477ffb8360c49c85c8ca8ef9700a4e6711f39a6e8b45/frozenlist-1.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:778a11b15673f6f1df23d9586f83c4846c471a8af693a22e066508b77d201ec8", size = 232015, upload-time = "2025-10-06T05:37:13.194Z" }, + { url = "https://files.pythonhosted.org/packages/dc/94/be719d2766c1138148564a3960fc2c06eb688da592bdc25adcf856101be7/frozenlist-1.8.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0325024fe97f94c41c08872db482cf8ac4800d80e79222c6b0b7b162d5b13686", size = 225038, upload-time = "2025-10-06T05:37:14.577Z" }, + { url = "https://files.pythonhosted.org/packages/e4/09/6712b6c5465f083f52f50cf74167b92d4ea2f50e46a9eea0523d658454ae/frozenlist-1.8.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:97260ff46b207a82a7567b581ab4190bd4dfa09f4db8a8b49d1a958f6aa4940e", size = 240130, upload-time = "2025-10-06T05:37:15.781Z" }, + { url = "https://files.pythonhosted.org/packages/f8/d4/cd065cdcf21550b54f3ce6a22e143ac9e4836ca42a0de1022da8498eac89/frozenlist-1.8.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:54b2077180eb7f83dd52c40b2750d0a9f175e06a42e3213ce047219de902717a", size = 242845, upload-time = "2025-10-06T05:37:17.037Z" }, + { url = "https://files.pythonhosted.org/packages/62/c3/f57a5c8c70cd1ead3d5d5f776f89d33110b1addae0ab010ad774d9a44fb9/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2f05983daecab868a31e1da44462873306d3cbfd76d1f0b5b69c473d21dbb128", size = 229131, upload-time = "2025-10-06T05:37:18.221Z" }, + { url = "https://files.pythonhosted.org/packages/6c/52/232476fe9cb64f0742f3fde2b7d26c1dac18b6d62071c74d4ded55e0ef94/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:33f48f51a446114bc5d251fb2954ab0164d5be02ad3382abcbfe07e2531d650f", size = 240542, upload-time = "2025-10-06T05:37:19.771Z" }, + { url = "https://files.pythonhosted.org/packages/5f/85/07bf3f5d0fb5414aee5f47d33c6f5c77bfe49aac680bfece33d4fdf6a246/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:154e55ec0655291b5dd1b8731c637ecdb50975a2ae70c606d100750a540082f7", size = 237308, upload-time = "2025-10-06T05:37:20.969Z" }, + { url = "https://files.pythonhosted.org/packages/11/99/ae3a33d5befd41ac0ca2cc7fd3aa707c9c324de2e89db0e0f45db9a64c26/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:4314debad13beb564b708b4a496020e5306c7333fa9a3ab90374169a20ffab30", size = 238210, upload-time = "2025-10-06T05:37:22.252Z" }, + { url = "https://files.pythonhosted.org/packages/b2/60/b1d2da22f4970e7a155f0adde9b1435712ece01b3cd45ba63702aea33938/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:073f8bf8becba60aa931eb3bc420b217bb7d5b8f4750e6f8b3be7f3da85d38b7", size = 231972, upload-time = "2025-10-06T05:37:23.5Z" }, + { url = "https://files.pythonhosted.org/packages/3f/ab/945b2f32de889993b9c9133216c068b7fcf257d8595a0ac420ac8677cab0/frozenlist-1.8.0-cp314-cp314-win32.whl", hash = "sha256:bac9c42ba2ac65ddc115d930c78d24ab8d4f465fd3fc473cdedfccadb9429806", size = 40536, upload-time = "2025-10-06T05:37:25.581Z" }, + { url = "https://files.pythonhosted.org/packages/59/ad/9caa9b9c836d9ad6f067157a531ac48b7d36499f5036d4141ce78c230b1b/frozenlist-1.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:3e0761f4d1a44f1d1a47996511752cf3dcec5bbdd9cc2b4fe595caf97754b7a0", size = 44330, upload-time = "2025-10-06T05:37:26.928Z" }, + { url = "https://files.pythonhosted.org/packages/82/13/e6950121764f2676f43534c555249f57030150260aee9dcf7d64efda11dd/frozenlist-1.8.0-cp314-cp314-win_arm64.whl", hash = "sha256:d1eaff1d00c7751b7c6662e9c5ba6eb2c17a2306ba5e2a37f24ddf3cc953402b", size = 40627, upload-time = "2025-10-06T05:37:28.075Z" }, + { url = "https://files.pythonhosted.org/packages/c0/c7/43200656ecc4e02d3f8bc248df68256cd9572b3f0017f0a0c4e93440ae23/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:d3bb933317c52d7ea5004a1c442eef86f426886fba134ef8cf4226ea6ee1821d", size = 89238, upload-time = "2025-10-06T05:37:29.373Z" }, + { url = "https://files.pythonhosted.org/packages/d1/29/55c5f0689b9c0fb765055629f472c0de484dcaf0acee2f7707266ae3583c/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:8009897cdef112072f93a0efdce29cd819e717fd2f649ee3016efd3cd885a7ed", size = 50738, upload-time = "2025-10-06T05:37:30.792Z" }, + { url = "https://files.pythonhosted.org/packages/ba/7d/b7282a445956506fa11da8c2db7d276adcbf2b17d8bb8407a47685263f90/frozenlist-1.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2c5dcbbc55383e5883246d11fd179782a9d07a986c40f49abe89ddf865913930", size = 51739, upload-time = "2025-10-06T05:37:32.127Z" }, + { url = "https://files.pythonhosted.org/packages/62/1c/3d8622e60d0b767a5510d1d3cf21065b9db874696a51ea6d7a43180a259c/frozenlist-1.8.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:39ecbc32f1390387d2aa4f5a995e465e9e2f79ba3adcac92d68e3e0afae6657c", size = 284186, upload-time = "2025-10-06T05:37:33.21Z" }, + { url = "https://files.pythonhosted.org/packages/2d/14/aa36d5f85a89679a85a1d44cd7a6657e0b1c75f61e7cad987b203d2daca8/frozenlist-1.8.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92db2bf818d5cc8d9c1f1fc56b897662e24ea5adb36ad1f1d82875bd64e03c24", size = 292196, upload-time = "2025-10-06T05:37:36.107Z" }, + { url = "https://files.pythonhosted.org/packages/05/23/6bde59eb55abd407d34f77d39a5126fb7b4f109a3f611d3929f14b700c66/frozenlist-1.8.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2dc43a022e555de94c3b68a4ef0b11c4f747d12c024a520c7101709a2144fb37", size = 273830, upload-time = "2025-10-06T05:37:37.663Z" }, + { url = "https://files.pythonhosted.org/packages/d2/3f/22cff331bfad7a8afa616289000ba793347fcd7bc275f3b28ecea2a27909/frozenlist-1.8.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb89a7f2de3602cfed448095bab3f178399646ab7c61454315089787df07733a", size = 294289, upload-time = "2025-10-06T05:37:39.261Z" }, + { url = "https://files.pythonhosted.org/packages/a4/89/5b057c799de4838b6c69aa82b79705f2027615e01be996d2486a69ca99c4/frozenlist-1.8.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:33139dc858c580ea50e7e60a1b0ea003efa1fd42e6ec7fdbad78fff65fad2fd2", size = 300318, upload-time = "2025-10-06T05:37:43.213Z" }, + { url = "https://files.pythonhosted.org/packages/30/de/2c22ab3eb2a8af6d69dc799e48455813bab3690c760de58e1bf43b36da3e/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:168c0969a329b416119507ba30b9ea13688fafffac1b7822802537569a1cb0ef", size = 282814, upload-time = "2025-10-06T05:37:45.337Z" }, + { url = "https://files.pythonhosted.org/packages/59/f7/970141a6a8dbd7f556d94977858cfb36fa9b66e0892c6dd780d2219d8cd8/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:28bd570e8e189d7f7b001966435f9dac6718324b5be2990ac496cf1ea9ddb7fe", size = 291762, upload-time = "2025-10-06T05:37:46.657Z" }, + { url = "https://files.pythonhosted.org/packages/c1/15/ca1adae83a719f82df9116d66f5bb28bb95557b3951903d39135620ef157/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b2a095d45c5d46e5e79ba1e5b9cb787f541a8dee0433836cea4b96a2c439dcd8", size = 289470, upload-time = "2025-10-06T05:37:47.946Z" }, + { url = "https://files.pythonhosted.org/packages/ac/83/dca6dc53bf657d371fbc88ddeb21b79891e747189c5de990b9dfff2ccba1/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:eab8145831a0d56ec9c4139b6c3e594c7a83c2c8be25d5bcf2d86136a532287a", size = 289042, upload-time = "2025-10-06T05:37:49.499Z" }, + { url = "https://files.pythonhosted.org/packages/96/52/abddd34ca99be142f354398700536c5bd315880ed0a213812bc491cff5e4/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:974b28cf63cc99dfb2188d8d222bc6843656188164848c4f679e63dae4b0708e", size = 283148, upload-time = "2025-10-06T05:37:50.745Z" }, + { url = "https://files.pythonhosted.org/packages/af/d3/76bd4ed4317e7119c2b7f57c3f6934aba26d277acc6309f873341640e21f/frozenlist-1.8.0-cp314-cp314t-win32.whl", hash = "sha256:342c97bf697ac5480c0a7ec73cd700ecfa5a8a40ac923bd035484616efecc2df", size = 44676, upload-time = "2025-10-06T05:37:52.222Z" }, + { url = "https://files.pythonhosted.org/packages/89/76/c615883b7b521ead2944bb3480398cbb07e12b7b4e4d073d3752eb721558/frozenlist-1.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:06be8f67f39c8b1dc671f5d83aaefd3358ae5cdcf8314552c57e7ed3e6475bdd", size = 49451, upload-time = "2025-10-06T05:37:53.425Z" }, + { url = "https://files.pythonhosted.org/packages/e0/a3/5982da14e113d07b325230f95060e2169f5311b1017ea8af2a29b374c289/frozenlist-1.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:102e6314ca4da683dca92e3b1355490fed5f313b768500084fbe6371fddfdb79", size = 42507, upload-time = "2025-10-06T05:37:54.513Z" }, + { url = "https://files.pythonhosted.org/packages/c2/59/ae5cdac87a00962122ea37bb346d41b66aec05f9ce328fa2b9e216f8967b/frozenlist-1.8.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:d8b7138e5cd0647e4523d6685b0eac5d4be9a184ae9634492f25c6eb38c12a47", size = 86967, upload-time = "2025-10-06T05:37:55.607Z" }, + { url = "https://files.pythonhosted.org/packages/8a/10/17059b2db5a032fd9323c41c39e9d1f5f9d0c8f04d1e4e3e788573086e61/frozenlist-1.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a6483e309ca809f1efd154b4d37dc6d9f61037d6c6a81c2dc7a15cb22c8c5dca", size = 49984, upload-time = "2025-10-06T05:37:57.049Z" }, + { url = "https://files.pythonhosted.org/packages/4b/de/ad9d82ca8e5fa8f0c636e64606553c79e2b859ad253030b62a21fe9986f5/frozenlist-1.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1b9290cf81e95e93fdf90548ce9d3c1211cf574b8e3f4b3b7cb0537cf2227068", size = 50240, upload-time = "2025-10-06T05:37:58.145Z" }, + { url = "https://files.pythonhosted.org/packages/4e/45/3dfb7767c2a67d123650122b62ce13c731b6c745bc14424eea67678b508c/frozenlist-1.8.0-cp39-cp39-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:59a6a5876ca59d1b63af8cd5e7ffffb024c3dc1e9cf9301b21a2e76286505c95", size = 219472, upload-time = "2025-10-06T05:37:59.239Z" }, + { url = "https://files.pythonhosted.org/packages/0b/bf/5bf23d913a741b960d5c1dac7c1985d8a2a1d015772b2d18ea168b08e7ff/frozenlist-1.8.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6dc4126390929823e2d2d9dc79ab4046ed74680360fc5f38b585c12c66cdf459", size = 221531, upload-time = "2025-10-06T05:38:00.521Z" }, + { url = "https://files.pythonhosted.org/packages/d0/03/27ec393f3b55860859f4b74cdc8c2a4af3dbf3533305e8eacf48a4fd9a54/frozenlist-1.8.0-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:332db6b2563333c5671fecacd085141b5800cb866be16d5e3eb15a2086476675", size = 219211, upload-time = "2025-10-06T05:38:01.842Z" }, + { url = "https://files.pythonhosted.org/packages/3a/ad/0fd00c404fa73fe9b169429e9a972d5ed807973c40ab6b3cf9365a33d360/frozenlist-1.8.0-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9ff15928d62a0b80bb875655c39bf517938c7d589554cbd2669be42d97c2cb61", size = 231775, upload-time = "2025-10-06T05:38:03.384Z" }, + { url = "https://files.pythonhosted.org/packages/8a/c3/86962566154cb4d2995358bc8331bfc4ea19d07db1a96f64935a1607f2b6/frozenlist-1.8.0-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7bf6cdf8e07c8151fba6fe85735441240ec7f619f935a5205953d58009aef8c6", size = 236631, upload-time = "2025-10-06T05:38:04.609Z" }, + { url = "https://files.pythonhosted.org/packages/ea/9e/6ffad161dbd83782d2c66dc4d378a9103b31770cb1e67febf43aea42d202/frozenlist-1.8.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:48e6d3f4ec5c7273dfe83ff27c91083c6c9065af655dc2684d2c200c94308bb5", size = 218632, upload-time = "2025-10-06T05:38:05.917Z" }, + { url = "https://files.pythonhosted.org/packages/58/b2/4677eee46e0a97f9b30735e6ad0bf6aba3e497986066eb68807ac85cf60f/frozenlist-1.8.0-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:1a7607e17ad33361677adcd1443edf6f5da0ce5e5377b798fba20fae194825f3", size = 235967, upload-time = "2025-10-06T05:38:07.614Z" }, + { url = "https://files.pythonhosted.org/packages/05/f3/86e75f8639c5a93745ca7addbbc9de6af56aebb930d233512b17e46f6493/frozenlist-1.8.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:5a3a935c3a4e89c733303a2d5a7c257ea44af3a56c8202df486b7f5de40f37e1", size = 228799, upload-time = "2025-10-06T05:38:08.845Z" }, + { url = "https://files.pythonhosted.org/packages/30/00/39aad3a7f0d98f5eb1d99a3c311215674ed87061aecee7851974b335c050/frozenlist-1.8.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:940d4a017dbfed9daf46a3b086e1d2167e7012ee297fef9e1c545c4d022f5178", size = 230566, upload-time = "2025-10-06T05:38:10.52Z" }, + { url = "https://files.pythonhosted.org/packages/0d/4d/aa144cac44568d137846ddc4d5210fb5d9719eb1d7ec6fa2728a54b5b94a/frozenlist-1.8.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:b9be22a69a014bc47e78072d0ecae716f5eb56c15238acca0f43d6eb8e4a5bda", size = 217715, upload-time = "2025-10-06T05:38:11.832Z" }, + { url = "https://files.pythonhosted.org/packages/64/4c/8f665921667509d25a0dd72540513bc86b356c95541686f6442a3283019f/frozenlist-1.8.0-cp39-cp39-win32.whl", hash = "sha256:1aa77cb5697069af47472e39612976ed05343ff2e84a3dcf15437b232cbfd087", size = 39933, upload-time = "2025-10-06T05:38:13.061Z" }, + { url = "https://files.pythonhosted.org/packages/79/bd/bcc926f87027fad5e59926ff12d136e1082a115025d33c032d1cd69ab377/frozenlist-1.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:7398c222d1d405e796970320036b1b563892b65809d9e5261487bb2c7f7b5c6a", size = 44121, upload-time = "2025-10-06T05:38:14.572Z" }, + { url = "https://files.pythonhosted.org/packages/4c/07/9c2e4eb7584af4b705237b971b89a4155a8e57599c4483a131a39256a9a0/frozenlist-1.8.0-cp39-cp39-win_arm64.whl", hash = "sha256:b4f3b365f31c6cd4af24545ca0a244a53688cad8834e32f56831c4923b50a103", size = 40312, upload-time = "2025-10-06T05:38:15.699Z" }, + { url = "https://files.pythonhosted.org/packages/9a/9a/e35b4a917281c0b8419d4207f4334c8e8c5dbf4f3f5f9ada73958d937dcc/frozenlist-1.8.0-py3-none-any.whl", hash = "sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d", size = 13409, upload-time = "2025-10-06T05:38:16.721Z" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + +[[package]] +name = "httpx-aiohttp" +version = "0.1.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohttp" }, + { name = "httpx" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d8/f2/9a86ce9bc48cf57dabb3a3160dfed26d8bbe5a2478a51f9d1dbf89f2f1fc/httpx_aiohttp-0.1.9.tar.gz", hash = "sha256:4ee8b22e6f2e7c80cd03be29eff98bfe7d89bd77f021ce0b578ee76b73b4bfe6", size = 206023, upload-time = "2025-10-15T08:52:57.475Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a1/db/5cfa8254a86c34a1ab7fe0dbec9f81bb5ebd831cbdd65aa4be4f37027804/httpx_aiohttp-0.1.9-py3-none-any.whl", hash = "sha256:3dc2845568b07742588710fcf3d72db2cbcdf2acc93376edf85f789c4d8e5fda", size = 6180, upload-time = "2025-10-15T08:52:56.521Z" }, +] + +[[package]] +name = "idna" +version = "3.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, +] + +[[package]] +name = "importlib-metadata" +version = "8.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "zipp" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/76/66/650a33bd90f786193e4de4b3ad86ea60b53c89b669a5c7be931fac31cdb0/importlib_metadata-8.7.0.tar.gz", hash = "sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000", size = 56641, upload-time = "2025-04-27T15:29:01.736Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/b0/36bd937216ec521246249be3bf9855081de4c5e06a0c9b4219dbeda50373/importlib_metadata-8.7.0-py3-none-any.whl", hash = "sha256:e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd", size = 27656, upload-time = "2025-04-27T15:29:00.214Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and extra != 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2'", + "python_full_version >= '3.10' and python_full_version < '3.14' and extra != 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2'", + "python_full_version >= '3.10' and extra == 'group-9-stagehand-pydantic-v1' and extra != 'group-9-stagehand-pydantic-v2'", + "python_full_version >= '3.10' and extra != 'group-9-stagehand-pydantic-v1' and extra != 'group-9-stagehand-pydantic-v2'", +] +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "3.0.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "mdurl", marker = "python_full_version < '3.10' or (extra == 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596, upload-time = "2023-06-03T06:41:14.443Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528, upload-time = "2023-06-03T06:41:11.019Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and extra != 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2'", + "python_full_version >= '3.10' and python_full_version < '3.14' and extra != 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2'", + "python_full_version >= '3.10' and extra == 'group-9-stagehand-pydantic-v1' and extra != 'group-9-stagehand-pydantic-v2'", + "python_full_version >= '3.10' and extra != 'group-9-stagehand-pydantic-v1' and extra != 'group-9-stagehand-pydantic-v2'", +] +dependencies = [ + { name = "mdurl", marker = "python_full_version >= '3.10' or (extra == 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + +[[package]] +name = "multidict" +version = "6.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.11' or (extra == 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/80/1e/5492c365f222f907de1039b91f922b93fa4f764c713ee858d235495d8f50/multidict-6.7.0.tar.gz", hash = "sha256:c6e99d9a65ca282e578dfea819cfa9c0a62b2499d8677392e09feaf305e9e6f5", size = 101834, upload-time = "2025-10-06T14:52:30.657Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a9/63/7bdd4adc330abcca54c85728db2327130e49e52e8c3ce685cec44e0f2e9f/multidict-6.7.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:9f474ad5acda359c8758c8accc22032c6abe6dc87a8be2440d097785e27a9349", size = 77153, upload-time = "2025-10-06T14:48:26.409Z" }, + { url = "https://files.pythonhosted.org/packages/3f/bb/b6c35ff175ed1a3142222b78455ee31be71a8396ed3ab5280fbe3ebe4e85/multidict-6.7.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4b7a9db5a870f780220e931d0002bbfd88fb53aceb6293251e2c839415c1b20e", size = 44993, upload-time = "2025-10-06T14:48:28.4Z" }, + { url = "https://files.pythonhosted.org/packages/e0/1f/064c77877c5fa6df6d346e68075c0f6998547afe952d6471b4c5f6a7345d/multidict-6.7.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:03ca744319864e92721195fa28c7a3b2bc7b686246b35e4078c1e4d0eb5466d3", size = 44607, upload-time = "2025-10-06T14:48:29.581Z" }, + { url = "https://files.pythonhosted.org/packages/04/7a/bf6aa92065dd47f287690000b3d7d332edfccb2277634cadf6a810463c6a/multidict-6.7.0-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f0e77e3c0008bc9316e662624535b88d360c3a5d3f81e15cf12c139a75250046", size = 241847, upload-time = "2025-10-06T14:48:32.107Z" }, + { url = "https://files.pythonhosted.org/packages/94/39/297a8de920f76eda343e4ce05f3b489f0ab3f9504f2576dfb37b7c08ca08/multidict-6.7.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:08325c9e5367aa379a3496aa9a022fe8837ff22e00b94db256d3a1378c76ab32", size = 242616, upload-time = "2025-10-06T14:48:34.054Z" }, + { url = "https://files.pythonhosted.org/packages/39/3a/d0eee2898cfd9d654aea6cb8c4addc2f9756e9a7e09391cfe55541f917f7/multidict-6.7.0-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e2862408c99f84aa571ab462d25236ef9cb12a602ea959ba9c9009a54902fc73", size = 222333, upload-time = "2025-10-06T14:48:35.9Z" }, + { url = "https://files.pythonhosted.org/packages/05/48/3b328851193c7a4240815b71eea165b49248867bbb6153a0aee227a0bb47/multidict-6.7.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4d72a9a2d885f5c208b0cb91ff2ed43636bb7e345ec839ff64708e04f69a13cc", size = 253239, upload-time = "2025-10-06T14:48:37.302Z" }, + { url = "https://files.pythonhosted.org/packages/b1/ca/0706a98c8d126a89245413225ca4a3fefc8435014de309cf8b30acb68841/multidict-6.7.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:478cc36476687bac1514d651cbbaa94b86b0732fb6855c60c673794c7dd2da62", size = 251618, upload-time = "2025-10-06T14:48:38.963Z" }, + { url = "https://files.pythonhosted.org/packages/5e/4f/9c7992f245554d8b173f6f0a048ad24b3e645d883f096857ec2c0822b8bd/multidict-6.7.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6843b28b0364dc605f21481c90fadb5f60d9123b442eb8a726bb74feef588a84", size = 241655, upload-time = "2025-10-06T14:48:40.312Z" }, + { url = "https://files.pythonhosted.org/packages/31/79/26a85991ae67efd1c0b1fc2e0c275b8a6aceeb155a68861f63f87a798f16/multidict-6.7.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:23bfeee5316266e5ee2d625df2d2c602b829435fc3a235c2ba2131495706e4a0", size = 239245, upload-time = "2025-10-06T14:48:41.848Z" }, + { url = "https://files.pythonhosted.org/packages/14/1e/75fa96394478930b79d0302eaf9a6c69f34005a1a5251ac8b9c336486ec9/multidict-6.7.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:680878b9f3d45c31e1f730eef731f9b0bc1da456155688c6745ee84eb818e90e", size = 233523, upload-time = "2025-10-06T14:48:43.749Z" }, + { url = "https://files.pythonhosted.org/packages/b2/5e/085544cb9f9c4ad2b5d97467c15f856df8d9bac410cffd5c43991a5d878b/multidict-6.7.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:eb866162ef2f45063acc7a53a88ef6fe8bf121d45c30ea3c9cd87ce7e191a8d4", size = 243129, upload-time = "2025-10-06T14:48:45.225Z" }, + { url = "https://files.pythonhosted.org/packages/b9/c3/e9d9e2f20c9474e7a8fcef28f863c5cbd29bb5adce6b70cebe8bdad0039d/multidict-6.7.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:df0e3bf7993bdbeca5ac25aa859cf40d39019e015c9c91809ba7093967f7a648", size = 248999, upload-time = "2025-10-06T14:48:46.703Z" }, + { url = "https://files.pythonhosted.org/packages/b5/3f/df171b6efa3239ae33b97b887e42671cd1d94d460614bfb2c30ffdab3b95/multidict-6.7.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:661709cdcd919a2ece2234f9bae7174e5220c80b034585d7d8a755632d3e2111", size = 243711, upload-time = "2025-10-06T14:48:48.146Z" }, + { url = "https://files.pythonhosted.org/packages/3c/2f/9b5564888c4e14b9af64c54acf149263721a283aaf4aa0ae89b091d5d8c1/multidict-6.7.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:096f52730c3fb8ed419db2d44391932b63891b2c5ed14850a7e215c0ba9ade36", size = 237504, upload-time = "2025-10-06T14:48:49.447Z" }, + { url = "https://files.pythonhosted.org/packages/6c/3a/0bd6ca0f7d96d790542d591c8c3354c1e1b6bfd2024d4d92dc3d87485ec7/multidict-6.7.0-cp310-cp310-win32.whl", hash = "sha256:afa8a2978ec65d2336305550535c9c4ff50ee527914328c8677b3973ade52b85", size = 41422, upload-time = "2025-10-06T14:48:50.789Z" }, + { url = "https://files.pythonhosted.org/packages/00/35/f6a637ea2c75f0d3b7c7d41b1189189acff0d9deeb8b8f35536bb30f5e33/multidict-6.7.0-cp310-cp310-win_amd64.whl", hash = "sha256:b15b3afff74f707b9275d5ba6a91ae8f6429c3ffb29bbfd216b0b375a56f13d7", size = 46050, upload-time = "2025-10-06T14:48:51.938Z" }, + { url = "https://files.pythonhosted.org/packages/e7/b8/f7bf8329b39893d02d9d95cf610c75885d12fc0f402b1c894e1c8e01c916/multidict-6.7.0-cp310-cp310-win_arm64.whl", hash = "sha256:4b73189894398d59131a66ff157837b1fafea9974be486d036bb3d32331fdbf0", size = 43153, upload-time = "2025-10-06T14:48:53.146Z" }, + { url = "https://files.pythonhosted.org/packages/34/9e/5c727587644d67b2ed479041e4b1c58e30afc011e3d45d25bbe35781217c/multidict-6.7.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:4d409aa42a94c0b3fa617708ef5276dfe81012ba6753a0370fcc9d0195d0a1fc", size = 76604, upload-time = "2025-10-06T14:48:54.277Z" }, + { url = "https://files.pythonhosted.org/packages/17/e4/67b5c27bd17c085a5ea8f1ec05b8a3e5cba0ca734bfcad5560fb129e70ca/multidict-6.7.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:14c9e076eede3b54c636f8ce1c9c252b5f057c62131211f0ceeec273810c9721", size = 44715, upload-time = "2025-10-06T14:48:55.445Z" }, + { url = "https://files.pythonhosted.org/packages/4d/e1/866a5d77be6ea435711bef2a4291eed11032679b6b28b56b4776ab06ba3e/multidict-6.7.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4c09703000a9d0fa3c3404b27041e574cc7f4df4c6563873246d0e11812a94b6", size = 44332, upload-time = "2025-10-06T14:48:56.706Z" }, + { url = "https://files.pythonhosted.org/packages/31/61/0c2d50241ada71ff61a79518db85ada85fdabfcf395d5968dae1cbda04e5/multidict-6.7.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a265acbb7bb33a3a2d626afbe756371dce0279e7b17f4f4eda406459c2b5ff1c", size = 245212, upload-time = "2025-10-06T14:48:58.042Z" }, + { url = "https://files.pythonhosted.org/packages/ac/e0/919666a4e4b57fff1b57f279be1c9316e6cdc5de8a8b525d76f6598fefc7/multidict-6.7.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:51cb455de290ae462593e5b1cb1118c5c22ea7f0d3620d9940bf695cea5a4bd7", size = 246671, upload-time = "2025-10-06T14:49:00.004Z" }, + { url = "https://files.pythonhosted.org/packages/a1/cc/d027d9c5a520f3321b65adea289b965e7bcbd2c34402663f482648c716ce/multidict-6.7.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:db99677b4457c7a5c5a949353e125ba72d62b35f74e26da141530fbb012218a7", size = 225491, upload-time = "2025-10-06T14:49:01.393Z" }, + { url = "https://files.pythonhosted.org/packages/75/c4/bbd633980ce6155a28ff04e6a6492dd3335858394d7bb752d8b108708558/multidict-6.7.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f470f68adc395e0183b92a2f4689264d1ea4b40504a24d9882c27375e6662bb9", size = 257322, upload-time = "2025-10-06T14:49:02.745Z" }, + { url = "https://files.pythonhosted.org/packages/4c/6d/d622322d344f1f053eae47e033b0b3f965af01212de21b10bcf91be991fb/multidict-6.7.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0db4956f82723cc1c270de9c6e799b4c341d327762ec78ef82bb962f79cc07d8", size = 254694, upload-time = "2025-10-06T14:49:04.15Z" }, + { url = "https://files.pythonhosted.org/packages/a8/9f/78f8761c2705d4c6d7516faed63c0ebdac569f6db1bef95e0d5218fdc146/multidict-6.7.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3e56d780c238f9e1ae66a22d2adf8d16f485381878250db8d496623cd38b22bd", size = 246715, upload-time = "2025-10-06T14:49:05.967Z" }, + { url = "https://files.pythonhosted.org/packages/78/59/950818e04f91b9c2b95aab3d923d9eabd01689d0dcd889563988e9ea0fd8/multidict-6.7.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9d14baca2ee12c1a64740d4531356ba50b82543017f3ad6de0deb943c5979abb", size = 243189, upload-time = "2025-10-06T14:49:07.37Z" }, + { url = "https://files.pythonhosted.org/packages/7a/3d/77c79e1934cad2ee74991840f8a0110966d9599b3af95964c0cd79bb905b/multidict-6.7.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:295a92a76188917c7f99cda95858c822f9e4aae5824246bba9b6b44004ddd0a6", size = 237845, upload-time = "2025-10-06T14:49:08.759Z" }, + { url = "https://files.pythonhosted.org/packages/63/1b/834ce32a0a97a3b70f86437f685f880136677ac00d8bce0027e9fd9c2db7/multidict-6.7.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:39f1719f57adbb767ef592a50ae5ebb794220d1188f9ca93de471336401c34d2", size = 246374, upload-time = "2025-10-06T14:49:10.574Z" }, + { url = "https://files.pythonhosted.org/packages/23/ef/43d1c3ba205b5dec93dc97f3fba179dfa47910fc73aaaea4f7ceb41cec2a/multidict-6.7.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:0a13fb8e748dfc94749f622de065dd5c1def7e0d2216dba72b1d8069a389c6ff", size = 253345, upload-time = "2025-10-06T14:49:12.331Z" }, + { url = "https://files.pythonhosted.org/packages/6b/03/eaf95bcc2d19ead522001f6a650ef32811aa9e3624ff0ad37c445c7a588c/multidict-6.7.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:e3aa16de190d29a0ea1b48253c57d99a68492c8dd8948638073ab9e74dc9410b", size = 246940, upload-time = "2025-10-06T14:49:13.821Z" }, + { url = "https://files.pythonhosted.org/packages/e8/df/ec8a5fd66ea6cd6f525b1fcbb23511b033c3e9bc42b81384834ffa484a62/multidict-6.7.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a048ce45dcdaaf1defb76b2e684f997fb5abf74437b6cb7b22ddad934a964e34", size = 242229, upload-time = "2025-10-06T14:49:15.603Z" }, + { url = "https://files.pythonhosted.org/packages/8a/a2/59b405d59fd39ec86d1142630e9049243015a5f5291ba49cadf3c090c541/multidict-6.7.0-cp311-cp311-win32.whl", hash = "sha256:a90af66facec4cebe4181b9e62a68be65e45ac9b52b67de9eec118701856e7ff", size = 41308, upload-time = "2025-10-06T14:49:16.871Z" }, + { url = "https://files.pythonhosted.org/packages/32/0f/13228f26f8b882c34da36efa776c3b7348455ec383bab4a66390e42963ae/multidict-6.7.0-cp311-cp311-win_amd64.whl", hash = "sha256:95b5ffa4349df2887518bb839409bcf22caa72d82beec453216802f475b23c81", size = 46037, upload-time = "2025-10-06T14:49:18.457Z" }, + { url = "https://files.pythonhosted.org/packages/84/1f/68588e31b000535a3207fd3c909ebeec4fb36b52c442107499c18a896a2a/multidict-6.7.0-cp311-cp311-win_arm64.whl", hash = "sha256:329aa225b085b6f004a4955271a7ba9f1087e39dcb7e65f6284a988264a63912", size = 43023, upload-time = "2025-10-06T14:49:19.648Z" }, + { url = "https://files.pythonhosted.org/packages/c2/9e/9f61ac18d9c8b475889f32ccfa91c9f59363480613fc807b6e3023d6f60b/multidict-6.7.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:8a3862568a36d26e650a19bb5cbbba14b71789032aebc0423f8cc5f150730184", size = 76877, upload-time = "2025-10-06T14:49:20.884Z" }, + { url = "https://files.pythonhosted.org/packages/38/6f/614f09a04e6184f8824268fce4bc925e9849edfa654ddd59f0b64508c595/multidict-6.7.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:960c60b5849b9b4f9dcc9bea6e3626143c252c74113df2c1540aebce70209b45", size = 45467, upload-time = "2025-10-06T14:49:22.054Z" }, + { url = "https://files.pythonhosted.org/packages/b3/93/c4f67a436dd026f2e780c433277fff72be79152894d9fc36f44569cab1a6/multidict-6.7.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2049be98fb57a31b4ccf870bf377af2504d4ae35646a19037ec271e4c07998aa", size = 43834, upload-time = "2025-10-06T14:49:23.566Z" }, + { url = "https://files.pythonhosted.org/packages/7f/f5/013798161ca665e4a422afbc5e2d9e4070142a9ff8905e482139cd09e4d0/multidict-6.7.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0934f3843a1860dd465d38895c17fce1f1cb37295149ab05cd1b9a03afacb2a7", size = 250545, upload-time = "2025-10-06T14:49:24.882Z" }, + { url = "https://files.pythonhosted.org/packages/71/2f/91dbac13e0ba94669ea5119ba267c9a832f0cb65419aca75549fcf09a3dc/multidict-6.7.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b3e34f3a1b8131ba06f1a73adab24f30934d148afcd5f5de9a73565a4404384e", size = 258305, upload-time = "2025-10-06T14:49:26.778Z" }, + { url = "https://files.pythonhosted.org/packages/ef/b0/754038b26f6e04488b48ac621f779c341338d78503fb45403755af2df477/multidict-6.7.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:efbb54e98446892590dc2458c19c10344ee9a883a79b5cec4bc34d6656e8d546", size = 242363, upload-time = "2025-10-06T14:49:28.562Z" }, + { url = "https://files.pythonhosted.org/packages/87/15/9da40b9336a7c9fa606c4cf2ed80a649dffeb42b905d4f63a1d7eb17d746/multidict-6.7.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a35c5fc61d4f51eb045061e7967cfe3123d622cd500e8868e7c0c592a09fedc4", size = 268375, upload-time = "2025-10-06T14:49:29.96Z" }, + { url = "https://files.pythonhosted.org/packages/82/72/c53fcade0cc94dfaad583105fd92b3a783af2091eddcb41a6d5a52474000/multidict-6.7.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29fe6740ebccba4175af1b9b87bf553e9c15cd5868ee967e010efcf94e4fd0f1", size = 269346, upload-time = "2025-10-06T14:49:31.404Z" }, + { url = "https://files.pythonhosted.org/packages/0d/e2/9baffdae21a76f77ef8447f1a05a96ec4bc0a24dae08767abc0a2fe680b8/multidict-6.7.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:123e2a72e20537add2f33a79e605f6191fba2afda4cbb876e35c1a7074298a7d", size = 256107, upload-time = "2025-10-06T14:49:32.974Z" }, + { url = "https://files.pythonhosted.org/packages/3c/06/3f06f611087dc60d65ef775f1fb5aca7c6d61c6db4990e7cda0cef9b1651/multidict-6.7.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b284e319754366c1aee2267a2036248b24eeb17ecd5dc16022095e747f2f4304", size = 253592, upload-time = "2025-10-06T14:49:34.52Z" }, + { url = "https://files.pythonhosted.org/packages/20/24/54e804ec7945b6023b340c412ce9c3f81e91b3bf5fa5ce65558740141bee/multidict-6.7.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:803d685de7be4303b5a657b76e2f6d1240e7e0a8aa2968ad5811fa2285553a12", size = 251024, upload-time = "2025-10-06T14:49:35.956Z" }, + { url = "https://files.pythonhosted.org/packages/14/48/011cba467ea0b17ceb938315d219391d3e421dfd35928e5dbdc3f4ae76ef/multidict-6.7.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c04a328260dfd5db8c39538f999f02779012268f54614902d0afc775d44e0a62", size = 251484, upload-time = "2025-10-06T14:49:37.631Z" }, + { url = "https://files.pythonhosted.org/packages/0d/2f/919258b43bb35b99fa127435cfb2d91798eb3a943396631ef43e3720dcf4/multidict-6.7.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8a19cdb57cd3df4cd865849d93ee14920fb97224300c88501f16ecfa2604b4e0", size = 263579, upload-time = "2025-10-06T14:49:39.502Z" }, + { url = "https://files.pythonhosted.org/packages/31/22/a0e884d86b5242b5a74cf08e876bdf299e413016b66e55511f7a804a366e/multidict-6.7.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:9b2fd74c52accced7e75de26023b7dccee62511a600e62311b918ec5c168fc2a", size = 259654, upload-time = "2025-10-06T14:49:41.32Z" }, + { url = "https://files.pythonhosted.org/packages/b2/e5/17e10e1b5c5f5a40f2fcbb45953c9b215f8a4098003915e46a93f5fcaa8f/multidict-6.7.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3e8bfdd0e487acf992407a140d2589fe598238eaeffa3da8448d63a63cd363f8", size = 251511, upload-time = "2025-10-06T14:49:46.021Z" }, + { url = "https://files.pythonhosted.org/packages/e3/9a/201bb1e17e7af53139597069c375e7b0dcbd47594604f65c2d5359508566/multidict-6.7.0-cp312-cp312-win32.whl", hash = "sha256:dd32a49400a2c3d52088e120ee00c1e3576cbff7e10b98467962c74fdb762ed4", size = 41895, upload-time = "2025-10-06T14:49:48.718Z" }, + { url = "https://files.pythonhosted.org/packages/46/e2/348cd32faad84eaf1d20cce80e2bb0ef8d312c55bca1f7fa9865e7770aaf/multidict-6.7.0-cp312-cp312-win_amd64.whl", hash = "sha256:92abb658ef2d7ef22ac9f8bb88e8b6c3e571671534e029359b6d9e845923eb1b", size = 46073, upload-time = "2025-10-06T14:49:50.28Z" }, + { url = "https://files.pythonhosted.org/packages/25/ec/aad2613c1910dce907480e0c3aa306905830f25df2e54ccc9dea450cb5aa/multidict-6.7.0-cp312-cp312-win_arm64.whl", hash = "sha256:490dab541a6a642ce1a9d61a4781656b346a55c13038f0b1244653828e3a83ec", size = 43226, upload-time = "2025-10-06T14:49:52.304Z" }, + { url = "https://files.pythonhosted.org/packages/d2/86/33272a544eeb36d66e4d9a920602d1a2f57d4ebea4ef3cdfe5a912574c95/multidict-6.7.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:bee7c0588aa0076ce77c0ea5d19a68d76ad81fcd9fe8501003b9a24f9d4000f6", size = 76135, upload-time = "2025-10-06T14:49:54.26Z" }, + { url = "https://files.pythonhosted.org/packages/91/1c/eb97db117a1ebe46d457a3d235a7b9d2e6dcab174f42d1b67663dd9e5371/multidict-6.7.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7ef6b61cad77091056ce0e7ce69814ef72afacb150b7ac6a3e9470def2198159", size = 45117, upload-time = "2025-10-06T14:49:55.82Z" }, + { url = "https://files.pythonhosted.org/packages/f1/d8/6c3442322e41fb1dd4de8bd67bfd11cd72352ac131f6368315617de752f1/multidict-6.7.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9c0359b1ec12b1d6849c59f9d319610b7f20ef990a6d454ab151aa0e3b9f78ca", size = 43472, upload-time = "2025-10-06T14:49:57.048Z" }, + { url = "https://files.pythonhosted.org/packages/75/3f/e2639e80325af0b6c6febdf8e57cc07043ff15f57fa1ef808f4ccb5ac4cd/multidict-6.7.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cd240939f71c64bd658f186330603aac1a9a81bf6273f523fca63673cb7378a8", size = 249342, upload-time = "2025-10-06T14:49:58.368Z" }, + { url = "https://files.pythonhosted.org/packages/5d/cc/84e0585f805cbeaa9cbdaa95f9a3d6aed745b9d25700623ac89a6ecff400/multidict-6.7.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a60a4d75718a5efa473ebd5ab685786ba0c67b8381f781d1be14da49f1a2dc60", size = 257082, upload-time = "2025-10-06T14:49:59.89Z" }, + { url = "https://files.pythonhosted.org/packages/b0/9c/ac851c107c92289acbbf5cfb485694084690c1b17e555f44952c26ddc5bd/multidict-6.7.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:53a42d364f323275126aff81fb67c5ca1b7a04fda0546245730a55c8c5f24bc4", size = 240704, upload-time = "2025-10-06T14:50:01.485Z" }, + { url = "https://files.pythonhosted.org/packages/50/cc/5f93e99427248c09da95b62d64b25748a5f5c98c7c2ab09825a1d6af0e15/multidict-6.7.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3b29b980d0ddbecb736735ee5bef69bb2ddca56eff603c86f3f29a1128299b4f", size = 266355, upload-time = "2025-10-06T14:50:02.955Z" }, + { url = "https://files.pythonhosted.org/packages/ec/0c/2ec1d883ceb79c6f7f6d7ad90c919c898f5d1c6ea96d322751420211e072/multidict-6.7.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f8a93b1c0ed2d04b97a5e9336fd2d33371b9a6e29ab7dd6503d63407c20ffbaf", size = 267259, upload-time = "2025-10-06T14:50:04.446Z" }, + { url = "https://files.pythonhosted.org/packages/c6/2d/f0b184fa88d6630aa267680bdb8623fb69cb0d024b8c6f0d23f9a0f406d3/multidict-6.7.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9ff96e8815eecacc6645da76c413eb3b3d34cfca256c70b16b286a687d013c32", size = 254903, upload-time = "2025-10-06T14:50:05.98Z" }, + { url = "https://files.pythonhosted.org/packages/06/c9/11ea263ad0df7dfabcad404feb3c0dd40b131bc7f232d5537f2fb1356951/multidict-6.7.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7516c579652f6a6be0e266aec0acd0db80829ca305c3d771ed898538804c2036", size = 252365, upload-time = "2025-10-06T14:50:07.511Z" }, + { url = "https://files.pythonhosted.org/packages/41/88/d714b86ee2c17d6e09850c70c9d310abac3d808ab49dfa16b43aba9d53fd/multidict-6.7.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:040f393368e63fb0f3330e70c26bfd336656bed925e5cbe17c9da839a6ab13ec", size = 250062, upload-time = "2025-10-06T14:50:09.074Z" }, + { url = "https://files.pythonhosted.org/packages/15/fe/ad407bb9e818c2b31383f6131ca19ea7e35ce93cf1310fce69f12e89de75/multidict-6.7.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b3bc26a951007b1057a1c543af845f1c7e3e71cc240ed1ace7bf4484aa99196e", size = 249683, upload-time = "2025-10-06T14:50:10.714Z" }, + { url = "https://files.pythonhosted.org/packages/8c/a4/a89abdb0229e533fb925e7c6e5c40201c2873efebc9abaf14046a4536ee6/multidict-6.7.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7b022717c748dd1992a83e219587aabe45980d88969f01b316e78683e6285f64", size = 261254, upload-time = "2025-10-06T14:50:12.28Z" }, + { url = "https://files.pythonhosted.org/packages/8d/aa/0e2b27bd88b40a4fb8dc53dd74eecac70edaa4c1dd0707eb2164da3675b3/multidict-6.7.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:9600082733859f00d79dee64effc7aef1beb26adb297416a4ad2116fd61374bd", size = 257967, upload-time = "2025-10-06T14:50:14.16Z" }, + { url = "https://files.pythonhosted.org/packages/d0/8e/0c67b7120d5d5f6d874ed85a085f9dc770a7f9d8813e80f44a9fec820bb7/multidict-6.7.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:94218fcec4d72bc61df51c198d098ce2b378e0ccbac41ddbed5ef44092913288", size = 250085, upload-time = "2025-10-06T14:50:15.639Z" }, + { url = "https://files.pythonhosted.org/packages/ba/55/b73e1d624ea4b8fd4dd07a3bb70f6e4c7c6c5d9d640a41c6ffe5cdbd2a55/multidict-6.7.0-cp313-cp313-win32.whl", hash = "sha256:a37bd74c3fa9d00be2d7b8eca074dc56bd8077ddd2917a839bd989612671ed17", size = 41713, upload-time = "2025-10-06T14:50:17.066Z" }, + { url = "https://files.pythonhosted.org/packages/32/31/75c59e7d3b4205075b4c183fa4ca398a2daf2303ddf616b04ae6ef55cffe/multidict-6.7.0-cp313-cp313-win_amd64.whl", hash = "sha256:30d193c6cc6d559db42b6bcec8a5d395d34d60c9877a0b71ecd7c204fcf15390", size = 45915, upload-time = "2025-10-06T14:50:18.264Z" }, + { url = "https://files.pythonhosted.org/packages/31/2a/8987831e811f1184c22bc2e45844934385363ee61c0a2dcfa8f71b87e608/multidict-6.7.0-cp313-cp313-win_arm64.whl", hash = "sha256:ea3334cabe4d41b7ccd01e4d349828678794edbc2d3ae97fc162a3312095092e", size = 43077, upload-time = "2025-10-06T14:50:19.853Z" }, + { url = "https://files.pythonhosted.org/packages/e8/68/7b3a5170a382a340147337b300b9eb25a9ddb573bcdfff19c0fa3f31ffba/multidict-6.7.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:ad9ce259f50abd98a1ca0aa6e490b58c316a0fce0617f609723e40804add2c00", size = 83114, upload-time = "2025-10-06T14:50:21.223Z" }, + { url = "https://files.pythonhosted.org/packages/55/5c/3fa2d07c84df4e302060f555bbf539310980362236ad49f50eeb0a1c1eb9/multidict-6.7.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:07f5594ac6d084cbb5de2df218d78baf55ef150b91f0ff8a21cc7a2e3a5a58eb", size = 48442, upload-time = "2025-10-06T14:50:22.871Z" }, + { url = "https://files.pythonhosted.org/packages/fc/56/67212d33239797f9bd91962bb899d72bb0f4c35a8652dcdb8ed049bef878/multidict-6.7.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:0591b48acf279821a579282444814a2d8d0af624ae0bc600aa4d1b920b6e924b", size = 46885, upload-time = "2025-10-06T14:50:24.258Z" }, + { url = "https://files.pythonhosted.org/packages/46/d1/908f896224290350721597a61a69cd19b89ad8ee0ae1f38b3f5cd12ea2ac/multidict-6.7.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:749a72584761531d2b9467cfbdfd29487ee21124c304c4b6cb760d8777b27f9c", size = 242588, upload-time = "2025-10-06T14:50:25.716Z" }, + { url = "https://files.pythonhosted.org/packages/ab/67/8604288bbd68680eee0ab568fdcb56171d8b23a01bcd5cb0c8fedf6e5d99/multidict-6.7.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b4c3d199f953acd5b446bf7c0de1fe25d94e09e79086f8dc2f48a11a129cdf1", size = 249966, upload-time = "2025-10-06T14:50:28.192Z" }, + { url = "https://files.pythonhosted.org/packages/20/33/9228d76339f1ba51e3efef7da3ebd91964d3006217aae13211653193c3ff/multidict-6.7.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:9fb0211dfc3b51efea2f349ec92c114d7754dd62c01f81c3e32b765b70c45c9b", size = 228618, upload-time = "2025-10-06T14:50:29.82Z" }, + { url = "https://files.pythonhosted.org/packages/f8/2d/25d9b566d10cab1c42b3b9e5b11ef79c9111eaf4463b8c257a3bd89e0ead/multidict-6.7.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a027ec240fe73a8d6281872690b988eed307cd7d91b23998ff35ff577ca688b5", size = 257539, upload-time = "2025-10-06T14:50:31.731Z" }, + { url = "https://files.pythonhosted.org/packages/b6/b1/8d1a965e6637fc33de3c0d8f414485c2b7e4af00f42cab3d84e7b955c222/multidict-6.7.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1d964afecdf3a8288789df2f5751dc0a8261138c3768d9af117ed384e538fad", size = 256345, upload-time = "2025-10-06T14:50:33.26Z" }, + { url = "https://files.pythonhosted.org/packages/ba/0c/06b5a8adbdeedada6f4fb8d8f193d44a347223b11939b42953eeb6530b6b/multidict-6.7.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:caf53b15b1b7df9fbd0709aa01409000a2b4dd03a5f6f5cc548183c7c8f8b63c", size = 247934, upload-time = "2025-10-06T14:50:34.808Z" }, + { url = "https://files.pythonhosted.org/packages/8f/31/b2491b5fe167ca044c6eb4b8f2c9f3b8a00b24c432c365358eadac5d7625/multidict-6.7.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:654030da3197d927f05a536a66186070e98765aa5142794c9904555d3a9d8fb5", size = 245243, upload-time = "2025-10-06T14:50:36.436Z" }, + { url = "https://files.pythonhosted.org/packages/61/1a/982913957cb90406c8c94f53001abd9eafc271cb3e70ff6371590bec478e/multidict-6.7.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:2090d3718829d1e484706a2f525e50c892237b2bf9b17a79b059cb98cddc2f10", size = 235878, upload-time = "2025-10-06T14:50:37.953Z" }, + { url = "https://files.pythonhosted.org/packages/be/c0/21435d804c1a1cf7a2608593f4d19bca5bcbd7a81a70b253fdd1c12af9c0/multidict-6.7.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:2d2cfeec3f6f45651b3d408c4acec0ebf3daa9bc8a112a084206f5db5d05b754", size = 243452, upload-time = "2025-10-06T14:50:39.574Z" }, + { url = "https://files.pythonhosted.org/packages/54/0a/4349d540d4a883863191be6eb9a928846d4ec0ea007d3dcd36323bb058ac/multidict-6.7.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:4ef089f985b8c194d341eb2c24ae6e7408c9a0e2e5658699c92f497437d88c3c", size = 252312, upload-time = "2025-10-06T14:50:41.612Z" }, + { url = "https://files.pythonhosted.org/packages/26/64/d5416038dbda1488daf16b676e4dbfd9674dde10a0cc8f4fc2b502d8125d/multidict-6.7.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:e93a0617cd16998784bf4414c7e40f17a35d2350e5c6f0bd900d3a8e02bd3762", size = 246935, upload-time = "2025-10-06T14:50:43.972Z" }, + { url = "https://files.pythonhosted.org/packages/9f/8c/8290c50d14e49f35e0bd4abc25e1bc7711149ca9588ab7d04f886cdf03d9/multidict-6.7.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f0feece2ef8ebc42ed9e2e8c78fc4aa3cf455733b507c09ef7406364c94376c6", size = 243385, upload-time = "2025-10-06T14:50:45.648Z" }, + { url = "https://files.pythonhosted.org/packages/ef/a0/f83ae75e42d694b3fbad3e047670e511c138be747bc713cf1b10d5096416/multidict-6.7.0-cp313-cp313t-win32.whl", hash = "sha256:19a1d55338ec1be74ef62440ca9e04a2f001a04d0cc49a4983dc320ff0f3212d", size = 47777, upload-time = "2025-10-06T14:50:47.154Z" }, + { url = "https://files.pythonhosted.org/packages/dc/80/9b174a92814a3830b7357307a792300f42c9e94664b01dee8e457551fa66/multidict-6.7.0-cp313-cp313t-win_amd64.whl", hash = "sha256:3da4fb467498df97e986af166b12d01f05d2e04f978a9c1c680ea1988e0bc4b6", size = 53104, upload-time = "2025-10-06T14:50:48.851Z" }, + { url = "https://files.pythonhosted.org/packages/cc/28/04baeaf0428d95bb7a7bea0e691ba2f31394338ba424fb0679a9ed0f4c09/multidict-6.7.0-cp313-cp313t-win_arm64.whl", hash = "sha256:b4121773c49a0776461f4a904cdf6264c88e42218aaa8407e803ca8025872792", size = 45503, upload-time = "2025-10-06T14:50:50.16Z" }, + { url = "https://files.pythonhosted.org/packages/e2/b1/3da6934455dd4b261d4c72f897e3a5728eba81db59959f3a639245891baa/multidict-6.7.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3bab1e4aff7adaa34410f93b1f8e57c4b36b9af0426a76003f441ee1d3c7e842", size = 75128, upload-time = "2025-10-06T14:50:51.92Z" }, + { url = "https://files.pythonhosted.org/packages/14/2c/f069cab5b51d175a1a2cb4ccdf7a2c2dabd58aa5bd933fa036a8d15e2404/multidict-6.7.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:b8512bac933afc3e45fb2b18da8e59b78d4f408399a960339598374d4ae3b56b", size = 44410, upload-time = "2025-10-06T14:50:53.275Z" }, + { url = "https://files.pythonhosted.org/packages/42/e2/64bb41266427af6642b6b128e8774ed84c11b80a90702c13ac0a86bb10cc/multidict-6.7.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:79dcf9e477bc65414ebfea98ffd013cb39552b5ecd62908752e0e413d6d06e38", size = 43205, upload-time = "2025-10-06T14:50:54.911Z" }, + { url = "https://files.pythonhosted.org/packages/02/68/6b086fef8a3f1a8541b9236c594f0c9245617c29841f2e0395d979485cde/multidict-6.7.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:31bae522710064b5cbeddaf2e9f32b1abab70ac6ac91d42572502299e9953128", size = 245084, upload-time = "2025-10-06T14:50:56.369Z" }, + { url = "https://files.pythonhosted.org/packages/15/ee/f524093232007cd7a75c1d132df70f235cfd590a7c9eaccd7ff422ef4ae8/multidict-6.7.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a0df7ff02397bb63e2fd22af2c87dfa39e8c7f12947bc524dbdc528282c7e34", size = 252667, upload-time = "2025-10-06T14:50:57.991Z" }, + { url = "https://files.pythonhosted.org/packages/02/a5/eeb3f43ab45878f1895118c3ef157a480db58ede3f248e29b5354139c2c9/multidict-6.7.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7a0222514e8e4c514660e182d5156a415c13ef0aabbd71682fc714e327b95e99", size = 233590, upload-time = "2025-10-06T14:50:59.589Z" }, + { url = "https://files.pythonhosted.org/packages/6a/1e/76d02f8270b97269d7e3dbd45644b1785bda457b474315f8cf999525a193/multidict-6.7.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2397ab4daaf2698eb51a76721e98db21ce4f52339e535725de03ea962b5a3202", size = 264112, upload-time = "2025-10-06T14:51:01.183Z" }, + { url = "https://files.pythonhosted.org/packages/76/0b/c28a70ecb58963847c2a8efe334904cd254812b10e535aefb3bcce513918/multidict-6.7.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8891681594162635948a636c9fe0ff21746aeb3dd5463f6e25d9bea3a8a39ca1", size = 261194, upload-time = "2025-10-06T14:51:02.794Z" }, + { url = "https://files.pythonhosted.org/packages/b4/63/2ab26e4209773223159b83aa32721b4021ffb08102f8ac7d689c943fded1/multidict-6.7.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18706cc31dbf402a7945916dd5cddf160251b6dab8a2c5f3d6d5a55949f676b3", size = 248510, upload-time = "2025-10-06T14:51:04.724Z" }, + { url = "https://files.pythonhosted.org/packages/93/cd/06c1fa8282af1d1c46fd55c10a7930af652afdce43999501d4d68664170c/multidict-6.7.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f844a1bbf1d207dd311a56f383f7eda2d0e134921d45751842d8235e7778965d", size = 248395, upload-time = "2025-10-06T14:51:06.306Z" }, + { url = "https://files.pythonhosted.org/packages/99/ac/82cb419dd6b04ccf9e7e61befc00c77614fc8134362488b553402ecd55ce/multidict-6.7.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:d4393e3581e84e5645506923816b9cc81f5609a778c7e7534054091acc64d1c6", size = 239520, upload-time = "2025-10-06T14:51:08.091Z" }, + { url = "https://files.pythonhosted.org/packages/fa/f3/a0f9bf09493421bd8716a362e0cd1d244f5a6550f5beffdd6b47e885b331/multidict-6.7.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:fbd18dc82d7bf274b37aa48d664534330af744e03bccf696d6f4c6042e7d19e7", size = 245479, upload-time = "2025-10-06T14:51:10.365Z" }, + { url = "https://files.pythonhosted.org/packages/8d/01/476d38fc73a212843f43c852b0eee266b6971f0e28329c2184a8df90c376/multidict-6.7.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:b6234e14f9314731ec45c42fc4554b88133ad53a09092cc48a88e771c125dadb", size = 258903, upload-time = "2025-10-06T14:51:12.466Z" }, + { url = "https://files.pythonhosted.org/packages/49/6d/23faeb0868adba613b817d0e69c5f15531b24d462af8012c4f6de4fa8dc3/multidict-6.7.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:08d4379f9744d8f78d98c8673c06e202ffa88296f009c71bbafe8a6bf847d01f", size = 252333, upload-time = "2025-10-06T14:51:14.48Z" }, + { url = "https://files.pythonhosted.org/packages/1e/cc/48d02ac22b30fa247f7dad82866e4b1015431092f4ba6ebc7e77596e0b18/multidict-6.7.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9fe04da3f79387f450fd0061d4dd2e45a72749d31bf634aecc9e27f24fdc4b3f", size = 243411, upload-time = "2025-10-06T14:51:16.072Z" }, + { url = "https://files.pythonhosted.org/packages/4a/03/29a8bf5a18abf1fe34535c88adbdfa88c9fb869b5a3b120692c64abe8284/multidict-6.7.0-cp314-cp314-win32.whl", hash = "sha256:fbafe31d191dfa7c4c51f7a6149c9fb7e914dcf9ffead27dcfd9f1ae382b3885", size = 40940, upload-time = "2025-10-06T14:51:17.544Z" }, + { url = "https://files.pythonhosted.org/packages/82/16/7ed27b680791b939de138f906d5cf2b4657b0d45ca6f5dd6236fdddafb1a/multidict-6.7.0-cp314-cp314-win_amd64.whl", hash = "sha256:2f67396ec0310764b9222a1728ced1ab638f61aadc6226f17a71dd9324f9a99c", size = 45087, upload-time = "2025-10-06T14:51:18.875Z" }, + { url = "https://files.pythonhosted.org/packages/cd/3c/e3e62eb35a1950292fe39315d3c89941e30a9d07d5d2df42965ab041da43/multidict-6.7.0-cp314-cp314-win_arm64.whl", hash = "sha256:ba672b26069957ee369cfa7fc180dde1fc6f176eaf1e6beaf61fbebbd3d9c000", size = 42368, upload-time = "2025-10-06T14:51:20.225Z" }, + { url = "https://files.pythonhosted.org/packages/8b/40/cd499bd0dbc5f1136726db3153042a735fffd0d77268e2ee20d5f33c010f/multidict-6.7.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:c1dcc7524066fa918c6a27d61444d4ee7900ec635779058571f70d042d86ed63", size = 82326, upload-time = "2025-10-06T14:51:21.588Z" }, + { url = "https://files.pythonhosted.org/packages/13/8a/18e031eca251c8df76daf0288e6790561806e439f5ce99a170b4af30676b/multidict-6.7.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:27e0b36c2d388dc7b6ced3406671b401e84ad7eb0656b8f3a2f46ed0ce483718", size = 48065, upload-time = "2025-10-06T14:51:22.93Z" }, + { url = "https://files.pythonhosted.org/packages/40/71/5e6701277470a87d234e433fb0a3a7deaf3bcd92566e421e7ae9776319de/multidict-6.7.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2a7baa46a22e77f0988e3b23d4ede5513ebec1929e34ee9495be535662c0dfe2", size = 46475, upload-time = "2025-10-06T14:51:24.352Z" }, + { url = "https://files.pythonhosted.org/packages/fe/6a/bab00cbab6d9cfb57afe1663318f72ec28289ea03fd4e8236bb78429893a/multidict-6.7.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7bf77f54997a9166a2f5675d1201520586439424c2511723a7312bdb4bcc034e", size = 239324, upload-time = "2025-10-06T14:51:25.822Z" }, + { url = "https://files.pythonhosted.org/packages/2a/5f/8de95f629fc22a7769ade8b41028e3e5a822c1f8904f618d175945a81ad3/multidict-6.7.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e011555abada53f1578d63389610ac8a5400fc70ce71156b0aa30d326f1a5064", size = 246877, upload-time = "2025-10-06T14:51:27.604Z" }, + { url = "https://files.pythonhosted.org/packages/23/b4/38881a960458f25b89e9f4a4fdcb02ac101cfa710190db6e5528841e67de/multidict-6.7.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:28b37063541b897fd6a318007373930a75ca6d6ac7c940dbe14731ffdd8d498e", size = 225824, upload-time = "2025-10-06T14:51:29.664Z" }, + { url = "https://files.pythonhosted.org/packages/1e/39/6566210c83f8a261575f18e7144736059f0c460b362e96e9cf797a24b8e7/multidict-6.7.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:05047ada7a2fde2631a0ed706f1fd68b169a681dfe5e4cf0f8e4cb6618bbc2cd", size = 253558, upload-time = "2025-10-06T14:51:31.684Z" }, + { url = "https://files.pythonhosted.org/packages/00/a3/67f18315100f64c269f46e6c0319fa87ba68f0f64f2b8e7fd7c72b913a0b/multidict-6.7.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:716133f7d1d946a4e1b91b1756b23c088881e70ff180c24e864c26192ad7534a", size = 252339, upload-time = "2025-10-06T14:51:33.699Z" }, + { url = "https://files.pythonhosted.org/packages/c8/2a/1cb77266afee2458d82f50da41beba02159b1d6b1f7973afc9a1cad1499b/multidict-6.7.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d1bed1b467ef657f2a0ae62844a607909ef1c6889562de5e1d505f74457d0b96", size = 244895, upload-time = "2025-10-06T14:51:36.189Z" }, + { url = "https://files.pythonhosted.org/packages/dd/72/09fa7dd487f119b2eb9524946ddd36e2067c08510576d43ff68469563b3b/multidict-6.7.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ca43bdfa5d37bd6aee89d85e1d0831fb86e25541be7e9d376ead1b28974f8e5e", size = 241862, upload-time = "2025-10-06T14:51:41.291Z" }, + { url = "https://files.pythonhosted.org/packages/65/92/bc1f8bd0853d8669300f732c801974dfc3702c3eeadae2f60cef54dc69d7/multidict-6.7.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:44b546bd3eb645fd26fb949e43c02a25a2e632e2ca21a35e2e132c8105dc8599", size = 232376, upload-time = "2025-10-06T14:51:43.55Z" }, + { url = "https://files.pythonhosted.org/packages/09/86/ac39399e5cb9d0c2ac8ef6e10a768e4d3bc933ac808d49c41f9dc23337eb/multidict-6.7.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:a6ef16328011d3f468e7ebc326f24c1445f001ca1dec335b2f8e66bed3006394", size = 240272, upload-time = "2025-10-06T14:51:45.265Z" }, + { url = "https://files.pythonhosted.org/packages/3d/b6/fed5ac6b8563ec72df6cb1ea8dac6d17f0a4a1f65045f66b6d3bf1497c02/multidict-6.7.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:5aa873cbc8e593d361ae65c68f85faadd755c3295ea2c12040ee146802f23b38", size = 248774, upload-time = "2025-10-06T14:51:46.836Z" }, + { url = "https://files.pythonhosted.org/packages/6b/8d/b954d8c0dc132b68f760aefd45870978deec6818897389dace00fcde32ff/multidict-6.7.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:3d7b6ccce016e29df4b7ca819659f516f0bc7a4b3efa3bb2012ba06431b044f9", size = 242731, upload-time = "2025-10-06T14:51:48.541Z" }, + { url = "https://files.pythonhosted.org/packages/16/9d/a2dac7009125d3540c2f54e194829ea18ac53716c61b655d8ed300120b0f/multidict-6.7.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:171b73bd4ee683d307599b66793ac80981b06f069b62eea1c9e29c9241aa66b0", size = 240193, upload-time = "2025-10-06T14:51:50.355Z" }, + { url = "https://files.pythonhosted.org/packages/39/ca/c05f144128ea232ae2178b008d5011d4e2cea86e4ee8c85c2631b1b94802/multidict-6.7.0-cp314-cp314t-win32.whl", hash = "sha256:b2d7f80c4e1fd010b07cb26820aae86b7e73b681ee4889684fb8d2d4537aab13", size = 48023, upload-time = "2025-10-06T14:51:51.883Z" }, + { url = "https://files.pythonhosted.org/packages/ba/8f/0a60e501584145588be1af5cc829265701ba3c35a64aec8e07cbb71d39bb/multidict-6.7.0-cp314-cp314t-win_amd64.whl", hash = "sha256:09929cab6fcb68122776d575e03c6cc64ee0b8fca48d17e135474b042ce515cd", size = 53507, upload-time = "2025-10-06T14:51:53.672Z" }, + { url = "https://files.pythonhosted.org/packages/7f/ae/3148b988a9c6239903e786eac19c889fab607c31d6efa7fb2147e5680f23/multidict-6.7.0-cp314-cp314t-win_arm64.whl", hash = "sha256:cc41db090ed742f32bd2d2c721861725e6109681eddf835d0a82bd3a5c382827", size = 44804, upload-time = "2025-10-06T14:51:55.415Z" }, + { url = "https://files.pythonhosted.org/packages/90/d7/4cf84257902265c4250769ac49f4eaab81c182ee9aff8bf59d2714dbb174/multidict-6.7.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:363eb68a0a59bd2303216d2346e6c441ba10d36d1f9969fcb6f1ba700de7bb5c", size = 77073, upload-time = "2025-10-06T14:51:57.386Z" }, + { url = "https://files.pythonhosted.org/packages/6d/51/194e999630a656e76c2965a1590d12faa5cd528170f2abaa04423e09fe8d/multidict-6.7.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d874eb056410ca05fed180b6642e680373688efafc7f077b2a2f61811e873a40", size = 44928, upload-time = "2025-10-06T14:51:58.791Z" }, + { url = "https://files.pythonhosted.org/packages/e5/6b/2a195373c33068c9158e0941d0b46cfcc9c1d894ca2eb137d1128081dff0/multidict-6.7.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8b55d5497b51afdfde55925e04a022f1de14d4f4f25cdfd4f5d9b0aa96166851", size = 44581, upload-time = "2025-10-06T14:52:00.174Z" }, + { url = "https://files.pythonhosted.org/packages/69/7b/7f4f2e644b6978bf011a5fd9a5ebb7c21de3f38523b1f7897d36a1ac1311/multidict-6.7.0-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f8e5c0031b90ca9ce555e2e8fd5c3b02a25f14989cbc310701823832c99eb687", size = 239901, upload-time = "2025-10-06T14:52:02.416Z" }, + { url = "https://files.pythonhosted.org/packages/3c/b5/952c72786710a031aa204a9adf7db66d7f97a2c6573889d58b9e60fe6702/multidict-6.7.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9cf41880c991716f3c7cec48e2f19ae4045fc9db5fc9cff27347ada24d710bb5", size = 240534, upload-time = "2025-10-06T14:52:04.105Z" }, + { url = "https://files.pythonhosted.org/packages/f3/ef/109fe1f2471e4c458c74242c7e4a833f2d9fc8a6813cd7ee345b0bad18f9/multidict-6.7.0-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8cfc12a8630a29d601f48d47787bd7eb730e475e83edb5d6c5084317463373eb", size = 219545, upload-time = "2025-10-06T14:52:06.208Z" }, + { url = "https://files.pythonhosted.org/packages/42/bd/327d91288114967f9fe90dc53de70aa3fec1b9073e46aa32c4828f771a87/multidict-6.7.0-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3996b50c3237c4aec17459217c1e7bbdead9a22a0fcd3c365564fbd16439dde6", size = 251187, upload-time = "2025-10-06T14:52:08.049Z" }, + { url = "https://files.pythonhosted.org/packages/f4/13/a8b078ebbaceb7819fd28cd004413c33b98f1b70d542a62e6a00b74fb09f/multidict-6.7.0-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7f5170993a0dd3ab871c74f45c0a21a4e2c37a2f2b01b5f722a2ad9c6650469e", size = 249379, upload-time = "2025-10-06T14:52:09.831Z" }, + { url = "https://files.pythonhosted.org/packages/e3/6d/ab12e1246be4d65d1f55de1e6f6aaa9b8120eddcfdd1d290439c7833d5ce/multidict-6.7.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ec81878ddf0e98817def1e77d4f50dae5ef5b0e4fe796fae3bd674304172416e", size = 239241, upload-time = "2025-10-06T14:52:11.561Z" }, + { url = "https://files.pythonhosted.org/packages/bb/d7/079a93625208c173b8fa756396814397c0fd9fee61ef87b75a748820b86e/multidict-6.7.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:9281bf5b34f59afbc6b1e477a372e9526b66ca446f4bf62592839c195a718b32", size = 237418, upload-time = "2025-10-06T14:52:13.671Z" }, + { url = "https://files.pythonhosted.org/packages/c9/29/03777c2212274aa9440918d604dc9d6af0e6b4558c611c32c3dcf1a13870/multidict-6.7.0-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:68af405971779d8b37198726f2b6fe3955db846fee42db7a4286fc542203934c", size = 232987, upload-time = "2025-10-06T14:52:15.708Z" }, + { url = "https://files.pythonhosted.org/packages/d9/00/11188b68d85a84e8050ee34724d6ded19ad03975caebe0c8dcb2829b37bf/multidict-6.7.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3ba3ef510467abb0667421a286dc906e30eb08569365f5cdb131d7aff7c2dd84", size = 240985, upload-time = "2025-10-06T14:52:17.317Z" }, + { url = "https://files.pythonhosted.org/packages/df/0c/12eef6aeda21859c6cdf7d75bd5516d83be3efe3d8cc45fd1a3037f5b9dc/multidict-6.7.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:b61189b29081a20c7e4e0b49b44d5d44bb0dc92be3c6d06a11cc043f81bf9329", size = 246855, upload-time = "2025-10-06T14:52:19.096Z" }, + { url = "https://files.pythonhosted.org/packages/69/f6/076120fd8bb3975f09228e288e08bff6b9f1bfd5166397c7ba284f622ab2/multidict-6.7.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:fb287618b9c7aa3bf8d825f02d9201b2f13078a5ed3b293c8f4d953917d84d5e", size = 241804, upload-time = "2025-10-06T14:52:21.166Z" }, + { url = "https://files.pythonhosted.org/packages/5f/51/41bb950c81437b88a93e6ddfca1d8763569ae861e638442838c4375f7497/multidict-6.7.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:521f33e377ff64b96c4c556b81c55d0cfffb96a11c194fd0c3f1e56f3d8dd5a4", size = 235321, upload-time = "2025-10-06T14:52:23.208Z" }, + { url = "https://files.pythonhosted.org/packages/5a/cf/5bbd31f055199d56c1f6b04bbadad3ccb24e6d5d4db75db774fc6d6674b8/multidict-6.7.0-cp39-cp39-win32.whl", hash = "sha256:ce8fdc2dca699f8dbf055a61d73eaa10482569ad20ee3c36ef9641f69afa8c91", size = 41435, upload-time = "2025-10-06T14:52:24.735Z" }, + { url = "https://files.pythonhosted.org/packages/af/01/547ffe9c2faec91c26965c152f3fea6cff068b6037401f61d310cc861ff4/multidict-6.7.0-cp39-cp39-win_amd64.whl", hash = "sha256:7e73299c99939f089dd9b2120a04a516b95cdf8c1cd2b18c53ebf0de80b1f18f", size = 46193, upload-time = "2025-10-06T14:52:26.101Z" }, + { url = "https://files.pythonhosted.org/packages/27/77/cfa5461d1d2651d6fc24216c92b4a21d4e385a41c46e0d9f3b070675167b/multidict-6.7.0-cp39-cp39-win_arm64.whl", hash = "sha256:6bdce131e14b04fd34a809b6380dbfd826065c3e2fe8a50dbae659fa0c390546", size = 43118, upload-time = "2025-10-06T14:52:27.876Z" }, + { url = "https://files.pythonhosted.org/packages/b7/da/7d22601b625e241d4f23ef1ebff8acfc60da633c9e7e7922e24d10f592b3/multidict-6.7.0-py3-none-any.whl", hash = "sha256:394fc5c42a333c9ffc3e421a4c85e08580d990e08b99f6bf35b4132114c5dcb3", size = 12317, upload-time = "2025-10-06T14:52:29.272Z" }, +] + +[[package]] +name = "mypy" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mypy-extensions" }, + { name = "pathspec" }, + { name = "tomli", marker = "python_full_version < '3.11' or (extra == 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2')" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1e/e3/034322d5a779685218ed69286c32faa505247f1f096251ef66c8fd203b08/mypy-1.17.0.tar.gz", hash = "sha256:e5d7ccc08ba089c06e2f5629c660388ef1fee708444f1dee0b9203fa031dee03", size = 3352114, upload-time = "2025-07-14T20:34:30.181Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/31/e762baa3b73905c856d45ab77b4af850e8159dffffd86a52879539a08c6b/mypy-1.17.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f8e08de6138043108b3b18f09d3f817a4783912e48828ab397ecf183135d84d6", size = 10998313, upload-time = "2025-07-14T20:33:24.519Z" }, + { url = "https://files.pythonhosted.org/packages/1c/c1/25b2f0d46fb7e0b5e2bee61ec3a47fe13eff9e3c2f2234f144858bbe6485/mypy-1.17.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ce4a17920ec144647d448fc43725b5873548b1aae6c603225626747ededf582d", size = 10128922, upload-time = "2025-07-14T20:34:06.414Z" }, + { url = "https://files.pythonhosted.org/packages/02/78/6d646603a57aa8a2886df1b8881fe777ea60f28098790c1089230cd9c61d/mypy-1.17.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6ff25d151cc057fdddb1cb1881ef36e9c41fa2a5e78d8dd71bee6e4dcd2bc05b", size = 11913524, upload-time = "2025-07-14T20:33:19.109Z" }, + { url = "https://files.pythonhosted.org/packages/4f/19/dae6c55e87ee426fb76980f7e78484450cad1c01c55a1dc4e91c930bea01/mypy-1.17.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:93468cf29aa9a132bceb103bd8475f78cacde2b1b9a94fd978d50d4bdf616c9a", size = 12650527, upload-time = "2025-07-14T20:32:44.095Z" }, + { url = "https://files.pythonhosted.org/packages/86/e1/f916845a235235a6c1e4d4d065a3930113767001d491b8b2e1b61ca56647/mypy-1.17.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:98189382b310f16343151f65dd7e6867386d3e35f7878c45cfa11383d175d91f", size = 12897284, upload-time = "2025-07-14T20:33:38.168Z" }, + { url = "https://files.pythonhosted.org/packages/ae/dc/414760708a4ea1b096bd214d26a24e30ac5e917ef293bc33cdb6fe22d2da/mypy-1.17.0-cp310-cp310-win_amd64.whl", hash = "sha256:c004135a300ab06a045c1c0d8e3f10215e71d7b4f5bb9a42ab80236364429937", size = 9506493, upload-time = "2025-07-14T20:34:01.093Z" }, + { url = "https://files.pythonhosted.org/packages/d4/24/82efb502b0b0f661c49aa21cfe3e1999ddf64bf5500fc03b5a1536a39d39/mypy-1.17.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9d4fe5c72fd262d9c2c91c1117d16aac555e05f5beb2bae6a755274c6eec42be", size = 10914150, upload-time = "2025-07-14T20:31:51.985Z" }, + { url = "https://files.pythonhosted.org/packages/03/96/8ef9a6ff8cedadff4400e2254689ca1dc4b420b92c55255b44573de10c54/mypy-1.17.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d96b196e5c16f41b4f7736840e8455958e832871990c7ba26bf58175e357ed61", size = 10039845, upload-time = "2025-07-14T20:32:30.527Z" }, + { url = "https://files.pythonhosted.org/packages/df/32/7ce359a56be779d38021d07941cfbb099b41411d72d827230a36203dbb81/mypy-1.17.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:73a0ff2dd10337ceb521c080d4147755ee302dcde6e1a913babd59473904615f", size = 11837246, upload-time = "2025-07-14T20:32:01.28Z" }, + { url = "https://files.pythonhosted.org/packages/82/16/b775047054de4d8dbd668df9137707e54b07fe18c7923839cd1e524bf756/mypy-1.17.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:24cfcc1179c4447854e9e406d3af0f77736d631ec87d31c6281ecd5025df625d", size = 12571106, upload-time = "2025-07-14T20:34:26.942Z" }, + { url = "https://files.pythonhosted.org/packages/a1/cf/fa33eaf29a606102c8d9ffa45a386a04c2203d9ad18bf4eef3e20c43ebc8/mypy-1.17.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3c56f180ff6430e6373db7a1d569317675b0a451caf5fef6ce4ab365f5f2f6c3", size = 12759960, upload-time = "2025-07-14T20:33:42.882Z" }, + { url = "https://files.pythonhosted.org/packages/94/75/3f5a29209f27e739ca57e6350bc6b783a38c7621bdf9cac3ab8a08665801/mypy-1.17.0-cp311-cp311-win_amd64.whl", hash = "sha256:eafaf8b9252734400f9b77df98b4eee3d2eecab16104680d51341c75702cad70", size = 9503888, upload-time = "2025-07-14T20:32:34.392Z" }, + { url = "https://files.pythonhosted.org/packages/12/e9/e6824ed620bbf51d3bf4d6cbbe4953e83eaf31a448d1b3cfb3620ccb641c/mypy-1.17.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f986f1cab8dbec39ba6e0eaa42d4d3ac6686516a5d3dccd64be095db05ebc6bb", size = 11086395, upload-time = "2025-07-14T20:34:11.452Z" }, + { url = "https://files.pythonhosted.org/packages/ba/51/a4afd1ae279707953be175d303f04a5a7bd7e28dc62463ad29c1c857927e/mypy-1.17.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:51e455a54d199dd6e931cd7ea987d061c2afbaf0960f7f66deef47c90d1b304d", size = 10120052, upload-time = "2025-07-14T20:33:09.897Z" }, + { url = "https://files.pythonhosted.org/packages/8a/71/19adfeac926ba8205f1d1466d0d360d07b46486bf64360c54cb5a2bd86a8/mypy-1.17.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3204d773bab5ff4ebbd1f8efa11b498027cd57017c003ae970f310e5b96be8d8", size = 11861806, upload-time = "2025-07-14T20:32:16.028Z" }, + { url = "https://files.pythonhosted.org/packages/0b/64/d6120eca3835baf7179e6797a0b61d6c47e0bc2324b1f6819d8428d5b9ba/mypy-1.17.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1051df7ec0886fa246a530ae917c473491e9a0ba6938cfd0ec2abc1076495c3e", size = 12744371, upload-time = "2025-07-14T20:33:33.503Z" }, + { url = "https://files.pythonhosted.org/packages/1f/dc/56f53b5255a166f5bd0f137eed960e5065f2744509dfe69474ff0ba772a5/mypy-1.17.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f773c6d14dcc108a5b141b4456b0871df638eb411a89cd1c0c001fc4a9d08fc8", size = 12914558, upload-time = "2025-07-14T20:33:56.961Z" }, + { url = "https://files.pythonhosted.org/packages/69/ac/070bad311171badc9add2910e7f89271695a25c136de24bbafc7eded56d5/mypy-1.17.0-cp312-cp312-win_amd64.whl", hash = "sha256:1619a485fd0e9c959b943c7b519ed26b712de3002d7de43154a489a2d0fd817d", size = 9585447, upload-time = "2025-07-14T20:32:20.594Z" }, + { url = "https://files.pythonhosted.org/packages/be/7b/5f8ab461369b9e62157072156935cec9d272196556bdc7c2ff5f4c7c0f9b/mypy-1.17.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2c41aa59211e49d717d92b3bb1238c06d387c9325d3122085113c79118bebb06", size = 11070019, upload-time = "2025-07-14T20:32:07.99Z" }, + { url = "https://files.pythonhosted.org/packages/9c/f8/c49c9e5a2ac0badcc54beb24e774d2499748302c9568f7f09e8730e953fa/mypy-1.17.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0e69db1fb65b3114f98c753e3930a00514f5b68794ba80590eb02090d54a5d4a", size = 10114457, upload-time = "2025-07-14T20:33:47.285Z" }, + { url = "https://files.pythonhosted.org/packages/89/0c/fb3f9c939ad9beed3e328008b3fb90b20fda2cddc0f7e4c20dbefefc3b33/mypy-1.17.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:03ba330b76710f83d6ac500053f7727270b6b8553b0423348ffb3af6f2f7b889", size = 11857838, upload-time = "2025-07-14T20:33:14.462Z" }, + { url = "https://files.pythonhosted.org/packages/4c/66/85607ab5137d65e4f54d9797b77d5a038ef34f714929cf8ad30b03f628df/mypy-1.17.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:037bc0f0b124ce46bfde955c647f3e395c6174476a968c0f22c95a8d2f589bba", size = 12731358, upload-time = "2025-07-14T20:32:25.579Z" }, + { url = "https://files.pythonhosted.org/packages/73/d0/341dbbfb35ce53d01f8f2969facbb66486cee9804048bf6c01b048127501/mypy-1.17.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c38876106cb6132259683632b287238858bd58de267d80defb6f418e9ee50658", size = 12917480, upload-time = "2025-07-14T20:34:21.868Z" }, + { url = "https://files.pythonhosted.org/packages/64/63/70c8b7dbfc520089ac48d01367a97e8acd734f65bd07813081f508a8c94c/mypy-1.17.0-cp313-cp313-win_amd64.whl", hash = "sha256:d30ba01c0f151998f367506fab31c2ac4527e6a7b2690107c7a7f9e3cb419a9c", size = 9589666, upload-time = "2025-07-14T20:34:16.841Z" }, + { url = "https://files.pythonhosted.org/packages/9f/a0/6263dd11941231f688f0a8f2faf90ceac1dc243d148d314a089d2fe25108/mypy-1.17.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:63e751f1b5ab51d6f3d219fe3a2fe4523eaa387d854ad06906c63883fde5b1ab", size = 10988185, upload-time = "2025-07-14T20:33:04.797Z" }, + { url = "https://files.pythonhosted.org/packages/02/13/b8f16d6b0dc80277129559c8e7dbc9011241a0da8f60d031edb0e6e9ac8f/mypy-1.17.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f7fb09d05e0f1c329a36dcd30e27564a3555717cde87301fae4fb542402ddfad", size = 10120169, upload-time = "2025-07-14T20:32:38.84Z" }, + { url = "https://files.pythonhosted.org/packages/14/ef/978ba79df0d65af680e20d43121363cf643eb79b04bf3880d01fc8afeb6f/mypy-1.17.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b72c34ce05ac3a1361ae2ebb50757fb6e3624032d91488d93544e9f82db0ed6c", size = 11918121, upload-time = "2025-07-14T20:33:52.328Z" }, + { url = "https://files.pythonhosted.org/packages/f4/10/55ef70b104151a0d8280474f05268ff0a2a79be8d788d5e647257d121309/mypy-1.17.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:434ad499ad8dde8b2f6391ddfa982f41cb07ccda8e3c67781b1bfd4e5f9450a8", size = 12648821, upload-time = "2025-07-14T20:32:59.631Z" }, + { url = "https://files.pythonhosted.org/packages/26/8c/7781fcd2e1eef48fbedd3a422c21fe300a8e03ed5be2eb4bd10246a77f4e/mypy-1.17.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:f105f61a5eff52e137fd73bee32958b2add9d9f0a856f17314018646af838e97", size = 12896955, upload-time = "2025-07-14T20:32:49.543Z" }, + { url = "https://files.pythonhosted.org/packages/78/13/03ac759dabe86e98ca7b6681f114f90ee03f3ff8365a57049d311bd4a4e3/mypy-1.17.0-cp39-cp39-win_amd64.whl", hash = "sha256:ba06254a5a22729853209550d80f94e28690d5530c661f9416a68ac097b13fc4", size = 9512957, upload-time = "2025-07-14T20:33:28.619Z" }, + { url = "https://files.pythonhosted.org/packages/e3/fc/ee058cc4316f219078464555873e99d170bde1d9569abd833300dbeb484a/mypy-1.17.0-py3-none-any.whl", hash = "sha256:15d9d0018237ab058e5de3d8fce61b6fa72cc59cc78fd91f1b474bce12abf496", size = 2283195, upload-time = "2025-07-14T20:31:54.753Z" }, +] + +[[package]] +name = "mypy-extensions" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, +] + +[[package]] +name = "nodeenv" +version = "1.9.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/16/fc88b08840de0e0a72a2f9d8c6bae36be573e475a6326ae854bcc549fc45/nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f", size = 47437, upload-time = "2024-06-04T18:44:11.171Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314, upload-time = "2024-06-04T18:44:08.352Z" }, +] + +[[package]] +name = "packaging" +version = "25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, +] + +[[package]] +name = "pathspec" +version = "0.12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043, upload-time = "2023-12-10T22:30:45Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191, upload-time = "2023-12-10T22:30:43.14Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "propcache" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9e/da/e9fc233cf63743258bff22b3dfa7ea5baef7b5bc324af47a0ad89b8ffc6f/propcache-0.4.1.tar.gz", hash = "sha256:f48107a8c637e80362555f37ecf49abe20370e557cc4ab374f04ec4423c97c3d", size = 46442, upload-time = "2025-10-08T19:49:02.291Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/0e/934b541323035566a9af292dba85a195f7b78179114f2c6ebb24551118a9/propcache-0.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7c2d1fa3201efaf55d730400d945b5b3ab6e672e100ba0f9a409d950ab25d7db", size = 79534, upload-time = "2025-10-08T19:46:02.083Z" }, + { url = "https://files.pythonhosted.org/packages/a1/6b/db0d03d96726d995dc7171286c6ba9d8d14251f37433890f88368951a44e/propcache-0.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1eb2994229cc8ce7fe9b3db88f5465f5fd8651672840b2e426b88cdb1a30aac8", size = 45526, upload-time = "2025-10-08T19:46:03.884Z" }, + { url = "https://files.pythonhosted.org/packages/e4/c3/82728404aea669e1600f304f2609cde9e665c18df5a11cdd57ed73c1dceb/propcache-0.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:66c1f011f45a3b33d7bcb22daed4b29c0c9e2224758b6be00686731e1b46f925", size = 47263, upload-time = "2025-10-08T19:46:05.405Z" }, + { url = "https://files.pythonhosted.org/packages/df/1b/39313ddad2bf9187a1432654c38249bab4562ef535ef07f5eb6eb04d0b1b/propcache-0.4.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9a52009f2adffe195d0b605c25ec929d26b36ef986ba85244891dee3b294df21", size = 201012, upload-time = "2025-10-08T19:46:07.165Z" }, + { url = "https://files.pythonhosted.org/packages/5b/01/f1d0b57d136f294a142acf97f4ed58c8e5b974c21e543000968357115011/propcache-0.4.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5d4e2366a9c7b837555cf02fb9be2e3167d333aff716332ef1b7c3a142ec40c5", size = 209491, upload-time = "2025-10-08T19:46:08.909Z" }, + { url = "https://files.pythonhosted.org/packages/a1/c8/038d909c61c5bb039070b3fb02ad5cccdb1dde0d714792e251cdb17c9c05/propcache-0.4.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:9d2b6caef873b4f09e26ea7e33d65f42b944837563a47a94719cc3544319a0db", size = 215319, upload-time = "2025-10-08T19:46:10.7Z" }, + { url = "https://files.pythonhosted.org/packages/08/57/8c87e93142b2c1fa2408e45695205a7ba05fb5db458c0bf5c06ba0e09ea6/propcache-0.4.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2b16ec437a8c8a965ecf95739448dd938b5c7f56e67ea009f4300d8df05f32b7", size = 196856, upload-time = "2025-10-08T19:46:12.003Z" }, + { url = "https://files.pythonhosted.org/packages/42/df/5615fec76aa561987a534759b3686008a288e73107faa49a8ae5795a9f7a/propcache-0.4.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:296f4c8ed03ca7476813fe666c9ea97869a8d7aec972618671b33a38a5182ef4", size = 193241, upload-time = "2025-10-08T19:46:13.495Z" }, + { url = "https://files.pythonhosted.org/packages/d5/21/62949eb3a7a54afe8327011c90aca7e03547787a88fb8bd9726806482fea/propcache-0.4.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:1f0978529a418ebd1f49dad413a2b68af33f85d5c5ca5c6ca2a3bed375a7ac60", size = 190552, upload-time = "2025-10-08T19:46:14.938Z" }, + { url = "https://files.pythonhosted.org/packages/30/ee/ab4d727dd70806e5b4de96a798ae7ac6e4d42516f030ee60522474b6b332/propcache-0.4.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:fd138803047fb4c062b1c1dd95462f5209456bfab55c734458f15d11da288f8f", size = 200113, upload-time = "2025-10-08T19:46:16.695Z" }, + { url = "https://files.pythonhosted.org/packages/8a/0b/38b46208e6711b016aa8966a3ac793eee0d05c7159d8342aa27fc0bc365e/propcache-0.4.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:8c9b3cbe4584636d72ff556d9036e0c9317fa27b3ac1f0f558e7e84d1c9c5900", size = 200778, upload-time = "2025-10-08T19:46:18.023Z" }, + { url = "https://files.pythonhosted.org/packages/cf/81/5abec54355ed344476bee711e9f04815d4b00a311ab0535599204eecc257/propcache-0.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f93243fdc5657247533273ac4f86ae106cc6445a0efacb9a1bfe982fcfefd90c", size = 193047, upload-time = "2025-10-08T19:46:19.449Z" }, + { url = "https://files.pythonhosted.org/packages/ec/b6/1f237c04e32063cb034acd5f6ef34ef3a394f75502e72703545631ab1ef6/propcache-0.4.1-cp310-cp310-win32.whl", hash = "sha256:a0ee98db9c5f80785b266eb805016e36058ac72c51a064040f2bc43b61101cdb", size = 38093, upload-time = "2025-10-08T19:46:20.643Z" }, + { url = "https://files.pythonhosted.org/packages/a6/67/354aac4e0603a15f76439caf0427781bcd6797f370377f75a642133bc954/propcache-0.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:1cdb7988c4e5ac7f6d175a28a9aa0c94cb6f2ebe52756a3c0cda98d2809a9e37", size = 41638, upload-time = "2025-10-08T19:46:21.935Z" }, + { url = "https://files.pythonhosted.org/packages/e0/e1/74e55b9fd1a4c209ff1a9a824bf6c8b3d1fc5a1ac3eabe23462637466785/propcache-0.4.1-cp310-cp310-win_arm64.whl", hash = "sha256:d82ad62b19645419fe79dd63b3f9253e15b30e955c0170e5cebc350c1844e581", size = 38229, upload-time = "2025-10-08T19:46:23.368Z" }, + { url = "https://files.pythonhosted.org/packages/8c/d4/4e2c9aaf7ac2242b9358f98dccd8f90f2605402f5afeff6c578682c2c491/propcache-0.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:60a8fda9644b7dfd5dece8c61d8a85e271cb958075bfc4e01083c148b61a7caf", size = 80208, upload-time = "2025-10-08T19:46:24.597Z" }, + { url = "https://files.pythonhosted.org/packages/c2/21/d7b68e911f9c8e18e4ae43bdbc1e1e9bbd971f8866eb81608947b6f585ff/propcache-0.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c30b53e7e6bda1d547cabb47c825f3843a0a1a42b0496087bb58d8fedf9f41b5", size = 45777, upload-time = "2025-10-08T19:46:25.733Z" }, + { url = "https://files.pythonhosted.org/packages/d3/1d/11605e99ac8ea9435651ee71ab4cb4bf03f0949586246476a25aadfec54a/propcache-0.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6918ecbd897443087a3b7cd978d56546a812517dcaaca51b49526720571fa93e", size = 47647, upload-time = "2025-10-08T19:46:27.304Z" }, + { url = "https://files.pythonhosted.org/packages/58/1a/3c62c127a8466c9c843bccb503d40a273e5cc69838805f322e2826509e0d/propcache-0.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3d902a36df4e5989763425a8ab9e98cd8ad5c52c823b34ee7ef307fd50582566", size = 214929, upload-time = "2025-10-08T19:46:28.62Z" }, + { url = "https://files.pythonhosted.org/packages/56/b9/8fa98f850960b367c4b8fe0592e7fc341daa7a9462e925228f10a60cf74f/propcache-0.4.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a9695397f85973bb40427dedddf70d8dc4a44b22f1650dd4af9eedf443d45165", size = 221778, upload-time = "2025-10-08T19:46:30.358Z" }, + { url = "https://files.pythonhosted.org/packages/46/a6/0ab4f660eb59649d14b3d3d65c439421cf2f87fe5dd68591cbe3c1e78a89/propcache-0.4.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2bb07ffd7eaad486576430c89f9b215f9e4be68c4866a96e97db9e97fead85dc", size = 228144, upload-time = "2025-10-08T19:46:32.607Z" }, + { url = "https://files.pythonhosted.org/packages/52/6a/57f43e054fb3d3a56ac9fc532bc684fc6169a26c75c353e65425b3e56eef/propcache-0.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fd6f30fdcf9ae2a70abd34da54f18da086160e4d7d9251f81f3da0ff84fc5a48", size = 210030, upload-time = "2025-10-08T19:46:33.969Z" }, + { url = "https://files.pythonhosted.org/packages/40/e2/27e6feebb5f6b8408fa29f5efbb765cd54c153ac77314d27e457a3e993b7/propcache-0.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:fc38cba02d1acba4e2869eef1a57a43dfbd3d49a59bf90dda7444ec2be6a5570", size = 208252, upload-time = "2025-10-08T19:46:35.309Z" }, + { url = "https://files.pythonhosted.org/packages/9e/f8/91c27b22ccda1dbc7967f921c42825564fa5336a01ecd72eb78a9f4f53c2/propcache-0.4.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:67fad6162281e80e882fb3ec355398cf72864a54069d060321f6cd0ade95fe85", size = 202064, upload-time = "2025-10-08T19:46:36.993Z" }, + { url = "https://files.pythonhosted.org/packages/f2/26/7f00bd6bd1adba5aafe5f4a66390f243acab58eab24ff1a08bebb2ef9d40/propcache-0.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f10207adf04d08bec185bae14d9606a1444715bc99180f9331c9c02093e1959e", size = 212429, upload-time = "2025-10-08T19:46:38.398Z" }, + { url = "https://files.pythonhosted.org/packages/84/89/fd108ba7815c1117ddca79c228f3f8a15fc82a73bca8b142eb5de13b2785/propcache-0.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:e9b0d8d0845bbc4cfcdcbcdbf5086886bc8157aa963c31c777ceff7846c77757", size = 216727, upload-time = "2025-10-08T19:46:39.732Z" }, + { url = "https://files.pythonhosted.org/packages/79/37/3ec3f7e3173e73f1d600495d8b545b53802cbf35506e5732dd8578db3724/propcache-0.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:981333cb2f4c1896a12f4ab92a9cc8f09ea664e9b7dbdc4eff74627af3a11c0f", size = 205097, upload-time = "2025-10-08T19:46:41.025Z" }, + { url = "https://files.pythonhosted.org/packages/61/b0/b2631c19793f869d35f47d5a3a56fb19e9160d3c119f15ac7344fc3ccae7/propcache-0.4.1-cp311-cp311-win32.whl", hash = "sha256:f1d2f90aeec838a52f1c1a32fe9a619fefd5e411721a9117fbf82aea638fe8a1", size = 38084, upload-time = "2025-10-08T19:46:42.693Z" }, + { url = "https://files.pythonhosted.org/packages/f4/78/6cce448e2098e9f3bfc91bb877f06aa24b6ccace872e39c53b2f707c4648/propcache-0.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:364426a62660f3f699949ac8c621aad6977be7126c5807ce48c0aeb8e7333ea6", size = 41637, upload-time = "2025-10-08T19:46:43.778Z" }, + { url = "https://files.pythonhosted.org/packages/9c/e9/754f180cccd7f51a39913782c74717c581b9cc8177ad0e949f4d51812383/propcache-0.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:e53f3a38d3510c11953f3e6a33f205c6d1b001129f972805ca9b42fc308bc239", size = 38064, upload-time = "2025-10-08T19:46:44.872Z" }, + { url = "https://files.pythonhosted.org/packages/a2/0f/f17b1b2b221d5ca28b4b876e8bb046ac40466513960646bda8e1853cdfa2/propcache-0.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e153e9cd40cc8945138822807139367f256f89c6810c2634a4f6902b52d3b4e2", size = 80061, upload-time = "2025-10-08T19:46:46.075Z" }, + { url = "https://files.pythonhosted.org/packages/76/47/8ccf75935f51448ba9a16a71b783eb7ef6b9ee60f5d14c7f8a8a79fbeed7/propcache-0.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cd547953428f7abb73c5ad82cbb32109566204260d98e41e5dfdc682eb7f8403", size = 46037, upload-time = "2025-10-08T19:46:47.23Z" }, + { url = "https://files.pythonhosted.org/packages/0a/b6/5c9a0e42df4d00bfb4a3cbbe5cf9f54260300c88a0e9af1f47ca5ce17ac0/propcache-0.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f048da1b4f243fc44f205dfd320933a951b8d89e0afd4c7cacc762a8b9165207", size = 47324, upload-time = "2025-10-08T19:46:48.384Z" }, + { url = "https://files.pythonhosted.org/packages/9e/d3/6c7ee328b39a81ee877c962469f1e795f9db87f925251efeb0545e0020d0/propcache-0.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ec17c65562a827bba85e3872ead335f95405ea1674860d96483a02f5c698fa72", size = 225505, upload-time = "2025-10-08T19:46:50.055Z" }, + { url = "https://files.pythonhosted.org/packages/01/5d/1c53f4563490b1d06a684742cc6076ef944bc6457df6051b7d1a877c057b/propcache-0.4.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:405aac25c6394ef275dee4c709be43745d36674b223ba4eb7144bf4d691b7367", size = 230242, upload-time = "2025-10-08T19:46:51.815Z" }, + { url = "https://files.pythonhosted.org/packages/20/e1/ce4620633b0e2422207c3cb774a0ee61cac13abc6217763a7b9e2e3f4a12/propcache-0.4.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0013cb6f8dde4b2a2f66903b8ba740bdfe378c943c4377a200551ceb27f379e4", size = 238474, upload-time = "2025-10-08T19:46:53.208Z" }, + { url = "https://files.pythonhosted.org/packages/46/4b/3aae6835b8e5f44ea6a68348ad90f78134047b503765087be2f9912140ea/propcache-0.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15932ab57837c3368b024473a525e25d316d8353016e7cc0e5ba9eb343fbb1cf", size = 221575, upload-time = "2025-10-08T19:46:54.511Z" }, + { url = "https://files.pythonhosted.org/packages/6e/a5/8a5e8678bcc9d3a1a15b9a29165640d64762d424a16af543f00629c87338/propcache-0.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:031dce78b9dc099f4c29785d9cf5577a3faf9ebf74ecbd3c856a7b92768c3df3", size = 216736, upload-time = "2025-10-08T19:46:56.212Z" }, + { url = "https://files.pythonhosted.org/packages/f1/63/b7b215eddeac83ca1c6b934f89d09a625aa9ee4ba158338854c87210cc36/propcache-0.4.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:ab08df6c9a035bee56e31af99be621526bd237bea9f32def431c656b29e41778", size = 213019, upload-time = "2025-10-08T19:46:57.595Z" }, + { url = "https://files.pythonhosted.org/packages/57/74/f580099a58c8af587cac7ba19ee7cb418506342fbbe2d4a4401661cca886/propcache-0.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4d7af63f9f93fe593afbf104c21b3b15868efb2c21d07d8732c0c4287e66b6a6", size = 220376, upload-time = "2025-10-08T19:46:59.067Z" }, + { url = "https://files.pythonhosted.org/packages/c4/ee/542f1313aff7eaf19c2bb758c5d0560d2683dac001a1c96d0774af799843/propcache-0.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:cfc27c945f422e8b5071b6e93169679e4eb5bf73bbcbf1ba3ae3a83d2f78ebd9", size = 226988, upload-time = "2025-10-08T19:47:00.544Z" }, + { url = "https://files.pythonhosted.org/packages/8f/18/9c6b015dd9c6930f6ce2229e1f02fb35298b847f2087ea2b436a5bfa7287/propcache-0.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:35c3277624a080cc6ec6f847cbbbb5b49affa3598c4535a0a4682a697aaa5c75", size = 215615, upload-time = "2025-10-08T19:47:01.968Z" }, + { url = "https://files.pythonhosted.org/packages/80/9e/e7b85720b98c45a45e1fca6a177024934dc9bc5f4d5dd04207f216fc33ed/propcache-0.4.1-cp312-cp312-win32.whl", hash = "sha256:671538c2262dadb5ba6395e26c1731e1d52534bfe9ae56d0b5573ce539266aa8", size = 38066, upload-time = "2025-10-08T19:47:03.503Z" }, + { url = "https://files.pythonhosted.org/packages/54/09/d19cff2a5aaac632ec8fc03737b223597b1e347416934c1b3a7df079784c/propcache-0.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:cb2d222e72399fcf5890d1d5cc1060857b9b236adff2792ff48ca2dfd46c81db", size = 41655, upload-time = "2025-10-08T19:47:04.973Z" }, + { url = "https://files.pythonhosted.org/packages/68/ab/6b5c191bb5de08036a8c697b265d4ca76148efb10fa162f14af14fb5f076/propcache-0.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:204483131fb222bdaaeeea9f9e6c6ed0cac32731f75dfc1d4a567fc1926477c1", size = 37789, upload-time = "2025-10-08T19:47:06.077Z" }, + { url = "https://files.pythonhosted.org/packages/bf/df/6d9c1b6ac12b003837dde8a10231a7344512186e87b36e855bef32241942/propcache-0.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:43eedf29202c08550aac1d14e0ee619b0430aaef78f85864c1a892294fbc28cf", size = 77750, upload-time = "2025-10-08T19:47:07.648Z" }, + { url = "https://files.pythonhosted.org/packages/8b/e8/677a0025e8a2acf07d3418a2e7ba529c9c33caf09d3c1f25513023c1db56/propcache-0.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d62cdfcfd89ccb8de04e0eda998535c406bf5e060ffd56be6c586cbcc05b3311", size = 44780, upload-time = "2025-10-08T19:47:08.851Z" }, + { url = "https://files.pythonhosted.org/packages/89/a4/92380f7ca60f99ebae761936bc48a72a639e8a47b29050615eef757cb2a7/propcache-0.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cae65ad55793da34db5f54e4029b89d3b9b9490d8abe1b4c7ab5d4b8ec7ebf74", size = 46308, upload-time = "2025-10-08T19:47:09.982Z" }, + { url = "https://files.pythonhosted.org/packages/2d/48/c5ac64dee5262044348d1d78a5f85dd1a57464a60d30daee946699963eb3/propcache-0.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:333ddb9031d2704a301ee3e506dc46b1fe5f294ec198ed6435ad5b6a085facfe", size = 208182, upload-time = "2025-10-08T19:47:11.319Z" }, + { url = "https://files.pythonhosted.org/packages/c6/0c/cd762dd011a9287389a6a3eb43aa30207bde253610cca06824aeabfe9653/propcache-0.4.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:fd0858c20f078a32cf55f7e81473d96dcf3b93fd2ccdb3d40fdf54b8573df3af", size = 211215, upload-time = "2025-10-08T19:47:13.146Z" }, + { url = "https://files.pythonhosted.org/packages/30/3e/49861e90233ba36890ae0ca4c660e95df565b2cd15d4a68556ab5865974e/propcache-0.4.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:678ae89ebc632c5c204c794f8dab2837c5f159aeb59e6ed0539500400577298c", size = 218112, upload-time = "2025-10-08T19:47:14.913Z" }, + { url = "https://files.pythonhosted.org/packages/f1/8b/544bc867e24e1bd48f3118cecd3b05c694e160a168478fa28770f22fd094/propcache-0.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d472aeb4fbf9865e0c6d622d7f4d54a4e101a89715d8904282bb5f9a2f476c3f", size = 204442, upload-time = "2025-10-08T19:47:16.277Z" }, + { url = "https://files.pythonhosted.org/packages/50/a6/4282772fd016a76d3e5c0df58380a5ea64900afd836cec2c2f662d1b9bb3/propcache-0.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4d3df5fa7e36b3225954fba85589da77a0fe6a53e3976de39caf04a0db4c36f1", size = 199398, upload-time = "2025-10-08T19:47:17.962Z" }, + { url = "https://files.pythonhosted.org/packages/3e/ec/d8a7cd406ee1ddb705db2139f8a10a8a427100347bd698e7014351c7af09/propcache-0.4.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:ee17f18d2498f2673e432faaa71698032b0127ebf23ae5974eeaf806c279df24", size = 196920, upload-time = "2025-10-08T19:47:19.355Z" }, + { url = "https://files.pythonhosted.org/packages/f6/6c/f38ab64af3764f431e359f8baf9e0a21013e24329e8b85d2da32e8ed07ca/propcache-0.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:580e97762b950f993ae618e167e7be9256b8353c2dcd8b99ec100eb50f5286aa", size = 203748, upload-time = "2025-10-08T19:47:21.338Z" }, + { url = "https://files.pythonhosted.org/packages/d6/e3/fa846bd70f6534d647886621388f0a265254d30e3ce47e5c8e6e27dbf153/propcache-0.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:501d20b891688eb8e7aa903021f0b72d5a55db40ffaab27edefd1027caaafa61", size = 205877, upload-time = "2025-10-08T19:47:23.059Z" }, + { url = "https://files.pythonhosted.org/packages/e2/39/8163fc6f3133fea7b5f2827e8eba2029a0277ab2c5beee6c1db7b10fc23d/propcache-0.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a0bd56e5b100aef69bd8562b74b46254e7c8812918d3baa700c8a8009b0af66", size = 199437, upload-time = "2025-10-08T19:47:24.445Z" }, + { url = "https://files.pythonhosted.org/packages/93/89/caa9089970ca49c7c01662bd0eeedfe85494e863e8043565aeb6472ce8fe/propcache-0.4.1-cp313-cp313-win32.whl", hash = "sha256:bcc9aaa5d80322bc2fb24bb7accb4a30f81e90ab8d6ba187aec0744bc302ad81", size = 37586, upload-time = "2025-10-08T19:47:25.736Z" }, + { url = "https://files.pythonhosted.org/packages/f5/ab/f76ec3c3627c883215b5c8080debb4394ef5a7a29be811f786415fc1e6fd/propcache-0.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:381914df18634f5494334d201e98245c0596067504b9372d8cf93f4bb23e025e", size = 40790, upload-time = "2025-10-08T19:47:26.847Z" }, + { url = "https://files.pythonhosted.org/packages/59/1b/e71ae98235f8e2ba5004d8cb19765a74877abf189bc53fc0c80d799e56c3/propcache-0.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:8873eb4460fd55333ea49b7d189749ecf6e55bf85080f11b1c4530ed3034cba1", size = 37158, upload-time = "2025-10-08T19:47:27.961Z" }, + { url = "https://files.pythonhosted.org/packages/83/ce/a31bbdfc24ee0dcbba458c8175ed26089cf109a55bbe7b7640ed2470cfe9/propcache-0.4.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:92d1935ee1f8d7442da9c0c4fa7ac20d07e94064184811b685f5c4fada64553b", size = 81451, upload-time = "2025-10-08T19:47:29.445Z" }, + { url = "https://files.pythonhosted.org/packages/25/9c/442a45a470a68456e710d96cacd3573ef26a1d0a60067e6a7d5e655621ed/propcache-0.4.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:473c61b39e1460d386479b9b2f337da492042447c9b685f28be4f74d3529e566", size = 46374, upload-time = "2025-10-08T19:47:30.579Z" }, + { url = "https://files.pythonhosted.org/packages/f4/bf/b1d5e21dbc3b2e889ea4327044fb16312a736d97640fb8b6aa3f9c7b3b65/propcache-0.4.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:c0ef0aaafc66fbd87842a3fe3902fd889825646bc21149eafe47be6072725835", size = 48396, upload-time = "2025-10-08T19:47:31.79Z" }, + { url = "https://files.pythonhosted.org/packages/f4/04/5b4c54a103d480e978d3c8a76073502b18db0c4bc17ab91b3cb5092ad949/propcache-0.4.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f95393b4d66bfae908c3ca8d169d5f79cd65636ae15b5e7a4f6e67af675adb0e", size = 275950, upload-time = "2025-10-08T19:47:33.481Z" }, + { url = "https://files.pythonhosted.org/packages/b4/c1/86f846827fb969c4b78b0af79bba1d1ea2156492e1b83dea8b8a6ae27395/propcache-0.4.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c07fda85708bc48578467e85099645167a955ba093be0a2dcba962195676e859", size = 273856, upload-time = "2025-10-08T19:47:34.906Z" }, + { url = "https://files.pythonhosted.org/packages/36/1d/fc272a63c8d3bbad6878c336c7a7dea15e8f2d23a544bda43205dfa83ada/propcache-0.4.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:af223b406d6d000830c6f65f1e6431783fc3f713ba3e6cc8c024d5ee96170a4b", size = 280420, upload-time = "2025-10-08T19:47:36.338Z" }, + { url = "https://files.pythonhosted.org/packages/07/0c/01f2219d39f7e53d52e5173bcb09c976609ba30209912a0680adfb8c593a/propcache-0.4.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a78372c932c90ee474559c5ddfffd718238e8673c340dc21fe45c5b8b54559a0", size = 263254, upload-time = "2025-10-08T19:47:37.692Z" }, + { url = "https://files.pythonhosted.org/packages/2d/18/cd28081658ce597898f0c4d174d4d0f3c5b6d4dc27ffafeef835c95eb359/propcache-0.4.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:564d9f0d4d9509e1a870c920a89b2fec951b44bf5ba7d537a9e7c1ccec2c18af", size = 261205, upload-time = "2025-10-08T19:47:39.659Z" }, + { url = "https://files.pythonhosted.org/packages/7a/71/1f9e22eb8b8316701c2a19fa1f388c8a3185082607da8e406a803c9b954e/propcache-0.4.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:17612831fda0138059cc5546f4d12a2aacfb9e47068c06af35c400ba58ba7393", size = 247873, upload-time = "2025-10-08T19:47:41.084Z" }, + { url = "https://files.pythonhosted.org/packages/4a/65/3d4b61f36af2b4eddba9def857959f1016a51066b4f1ce348e0cf7881f58/propcache-0.4.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:41a89040cb10bd345b3c1a873b2bf36413d48da1def52f268a055f7398514874", size = 262739, upload-time = "2025-10-08T19:47:42.51Z" }, + { url = "https://files.pythonhosted.org/packages/2a/42/26746ab087faa77c1c68079b228810436ccd9a5ce9ac85e2b7307195fd06/propcache-0.4.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:e35b88984e7fa64aacecea39236cee32dd9bd8c55f57ba8a75cf2399553f9bd7", size = 263514, upload-time = "2025-10-08T19:47:43.927Z" }, + { url = "https://files.pythonhosted.org/packages/94/13/630690fe201f5502d2403dd3cfd451ed8858fe3c738ee88d095ad2ff407b/propcache-0.4.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f8b465489f927b0df505cbe26ffbeed4d6d8a2bbc61ce90eb074ff129ef0ab1", size = 257781, upload-time = "2025-10-08T19:47:45.448Z" }, + { url = "https://files.pythonhosted.org/packages/92/f7/1d4ec5841505f423469efbfc381d64b7b467438cd5a4bbcbb063f3b73d27/propcache-0.4.1-cp313-cp313t-win32.whl", hash = "sha256:2ad890caa1d928c7c2965b48f3a3815c853180831d0e5503d35cf00c472f4717", size = 41396, upload-time = "2025-10-08T19:47:47.202Z" }, + { url = "https://files.pythonhosted.org/packages/48/f0/615c30622316496d2cbbc29f5985f7777d3ada70f23370608c1d3e081c1f/propcache-0.4.1-cp313-cp313t-win_amd64.whl", hash = "sha256:f7ee0e597f495cf415bcbd3da3caa3bd7e816b74d0d52b8145954c5e6fd3ff37", size = 44897, upload-time = "2025-10-08T19:47:48.336Z" }, + { url = "https://files.pythonhosted.org/packages/fd/ca/6002e46eccbe0e33dcd4069ef32f7f1c9e243736e07adca37ae8c4830ec3/propcache-0.4.1-cp313-cp313t-win_arm64.whl", hash = "sha256:929d7cbe1f01bb7baffb33dc14eb5691c95831450a26354cd210a8155170c93a", size = 39789, upload-time = "2025-10-08T19:47:49.876Z" }, + { url = "https://files.pythonhosted.org/packages/8e/5c/bca52d654a896f831b8256683457ceddd490ec18d9ec50e97dfd8fc726a8/propcache-0.4.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3f7124c9d820ba5548d431afb4632301acf965db49e666aa21c305cbe8c6de12", size = 78152, upload-time = "2025-10-08T19:47:51.051Z" }, + { url = "https://files.pythonhosted.org/packages/65/9b/03b04e7d82a5f54fb16113d839f5ea1ede58a61e90edf515f6577c66fa8f/propcache-0.4.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:c0d4b719b7da33599dfe3b22d3db1ef789210a0597bc650b7cee9c77c2be8c5c", size = 44869, upload-time = "2025-10-08T19:47:52.594Z" }, + { url = "https://files.pythonhosted.org/packages/b2/fa/89a8ef0468d5833a23fff277b143d0573897cf75bd56670a6d28126c7d68/propcache-0.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9f302f4783709a78240ebc311b793f123328716a60911d667e0c036bc5dcbded", size = 46596, upload-time = "2025-10-08T19:47:54.073Z" }, + { url = "https://files.pythonhosted.org/packages/86/bd/47816020d337f4a746edc42fe8d53669965138f39ee117414c7d7a340cfe/propcache-0.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c80ee5802e3fb9ea37938e7eecc307fb984837091d5fd262bb37238b1ae97641", size = 206981, upload-time = "2025-10-08T19:47:55.715Z" }, + { url = "https://files.pythonhosted.org/packages/df/f6/c5fa1357cc9748510ee55f37173eb31bfde6d94e98ccd9e6f033f2fc06e1/propcache-0.4.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ed5a841e8bb29a55fb8159ed526b26adc5bdd7e8bd7bf793ce647cb08656cdf4", size = 211490, upload-time = "2025-10-08T19:47:57.499Z" }, + { url = "https://files.pythonhosted.org/packages/80/1e/e5889652a7c4a3846683401a48f0f2e5083ce0ec1a8a5221d8058fbd1adf/propcache-0.4.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:55c72fd6ea2da4c318e74ffdf93c4fe4e926051133657459131a95c846d16d44", size = 215371, upload-time = "2025-10-08T19:47:59.317Z" }, + { url = "https://files.pythonhosted.org/packages/b2/f2/889ad4b2408f72fe1a4f6a19491177b30ea7bf1a0fd5f17050ca08cfc882/propcache-0.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8326e144341460402713f91df60ade3c999d601e7eb5ff8f6f7862d54de0610d", size = 201424, upload-time = "2025-10-08T19:48:00.67Z" }, + { url = "https://files.pythonhosted.org/packages/27/73/033d63069b57b0812c8bd19f311faebeceb6ba31b8f32b73432d12a0b826/propcache-0.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:060b16ae65bc098da7f6d25bf359f1f31f688384858204fe5d652979e0015e5b", size = 197566, upload-time = "2025-10-08T19:48:02.604Z" }, + { url = "https://files.pythonhosted.org/packages/dc/89/ce24f3dc182630b4e07aa6d15f0ff4b14ed4b9955fae95a0b54c58d66c05/propcache-0.4.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:89eb3fa9524f7bec9de6e83cf3faed9d79bffa560672c118a96a171a6f55831e", size = 193130, upload-time = "2025-10-08T19:48:04.499Z" }, + { url = "https://files.pythonhosted.org/packages/a9/24/ef0d5fd1a811fb5c609278d0209c9f10c35f20581fcc16f818da959fc5b4/propcache-0.4.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:dee69d7015dc235f526fe80a9c90d65eb0039103fe565776250881731f06349f", size = 202625, upload-time = "2025-10-08T19:48:06.213Z" }, + { url = "https://files.pythonhosted.org/packages/f5/02/98ec20ff5546f68d673df2f7a69e8c0d076b5abd05ca882dc7ee3a83653d/propcache-0.4.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:5558992a00dfd54ccbc64a32726a3357ec93825a418a401f5cc67df0ac5d9e49", size = 204209, upload-time = "2025-10-08T19:48:08.432Z" }, + { url = "https://files.pythonhosted.org/packages/a0/87/492694f76759b15f0467a2a93ab68d32859672b646aa8a04ce4864e7932d/propcache-0.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c9b822a577f560fbd9554812526831712c1436d2c046cedee4c3796d3543b144", size = 197797, upload-time = "2025-10-08T19:48:09.968Z" }, + { url = "https://files.pythonhosted.org/packages/ee/36/66367de3575db1d2d3f3d177432bd14ee577a39d3f5d1b3d5df8afe3b6e2/propcache-0.4.1-cp314-cp314-win32.whl", hash = "sha256:ab4c29b49d560fe48b696cdcb127dd36e0bc2472548f3bf56cc5cb3da2b2984f", size = 38140, upload-time = "2025-10-08T19:48:11.232Z" }, + { url = "https://files.pythonhosted.org/packages/0c/2a/a758b47de253636e1b8aef181c0b4f4f204bf0dd964914fb2af90a95b49b/propcache-0.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:5a103c3eb905fcea0ab98be99c3a9a5ab2de60228aa5aceedc614c0281cf6153", size = 41257, upload-time = "2025-10-08T19:48:12.707Z" }, + { url = "https://files.pythonhosted.org/packages/34/5e/63bd5896c3fec12edcbd6f12508d4890d23c265df28c74b175e1ef9f4f3b/propcache-0.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:74c1fb26515153e482e00177a1ad654721bf9207da8a494a0c05e797ad27b992", size = 38097, upload-time = "2025-10-08T19:48:13.923Z" }, + { url = "https://files.pythonhosted.org/packages/99/85/9ff785d787ccf9bbb3f3106f79884a130951436f58392000231b4c737c80/propcache-0.4.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:824e908bce90fb2743bd6b59db36eb4f45cd350a39637c9f73b1c1ea66f5b75f", size = 81455, upload-time = "2025-10-08T19:48:15.16Z" }, + { url = "https://files.pythonhosted.org/packages/90/85/2431c10c8e7ddb1445c1f7c4b54d886e8ad20e3c6307e7218f05922cad67/propcache-0.4.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:c2b5e7db5328427c57c8e8831abda175421b709672f6cfc3d630c3b7e2146393", size = 46372, upload-time = "2025-10-08T19:48:16.424Z" }, + { url = "https://files.pythonhosted.org/packages/01/20/b0972d902472da9bcb683fa595099911f4d2e86e5683bcc45de60dd05dc3/propcache-0.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6f6ff873ed40292cd4969ef5310179afd5db59fdf055897e282485043fc80ad0", size = 48411, upload-time = "2025-10-08T19:48:17.577Z" }, + { url = "https://files.pythonhosted.org/packages/e2/e3/7dc89f4f21e8f99bad3d5ddb3a3389afcf9da4ac69e3deb2dcdc96e74169/propcache-0.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:49a2dc67c154db2c1463013594c458881a069fcf98940e61a0569016a583020a", size = 275712, upload-time = "2025-10-08T19:48:18.901Z" }, + { url = "https://files.pythonhosted.org/packages/20/67/89800c8352489b21a8047c773067644e3897f02ecbbd610f4d46b7f08612/propcache-0.4.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:005f08e6a0529984491e37d8dbc3dd86f84bd78a8ceb5fa9a021f4c48d4984be", size = 273557, upload-time = "2025-10-08T19:48:20.762Z" }, + { url = "https://files.pythonhosted.org/packages/e2/a1/b52b055c766a54ce6d9c16d9aca0cad8059acd9637cdf8aa0222f4a026ef/propcache-0.4.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5c3310452e0d31390da9035c348633b43d7e7feb2e37be252be6da45abd1abcc", size = 280015, upload-time = "2025-10-08T19:48:22.592Z" }, + { url = "https://files.pythonhosted.org/packages/48/c8/33cee30bd890672c63743049f3c9e4be087e6780906bfc3ec58528be59c1/propcache-0.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c3c70630930447f9ef1caac7728c8ad1c56bc5015338b20fed0d08ea2480b3a", size = 262880, upload-time = "2025-10-08T19:48:23.947Z" }, + { url = "https://files.pythonhosted.org/packages/0c/b1/8f08a143b204b418285c88b83d00edbd61afbc2c6415ffafc8905da7038b/propcache-0.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8e57061305815dfc910a3634dcf584f08168a8836e6999983569f51a8544cd89", size = 260938, upload-time = "2025-10-08T19:48:25.656Z" }, + { url = "https://files.pythonhosted.org/packages/cf/12/96e4664c82ca2f31e1c8dff86afb867348979eb78d3cb8546a680287a1e9/propcache-0.4.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:521a463429ef54143092c11a77e04056dd00636f72e8c45b70aaa3140d639726", size = 247641, upload-time = "2025-10-08T19:48:27.207Z" }, + { url = "https://files.pythonhosted.org/packages/18/ed/e7a9cfca28133386ba52278136d42209d3125db08d0a6395f0cba0c0285c/propcache-0.4.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:120c964da3fdc75e3731aa392527136d4ad35868cc556fd09bb6d09172d9a367", size = 262510, upload-time = "2025-10-08T19:48:28.65Z" }, + { url = "https://files.pythonhosted.org/packages/f5/76/16d8bf65e8845dd62b4e2b57444ab81f07f40caa5652b8969b87ddcf2ef6/propcache-0.4.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:d8f353eb14ee3441ee844ade4277d560cdd68288838673273b978e3d6d2c8f36", size = 263161, upload-time = "2025-10-08T19:48:30.133Z" }, + { url = "https://files.pythonhosted.org/packages/e7/70/c99e9edb5d91d5ad8a49fa3c1e8285ba64f1476782fed10ab251ff413ba1/propcache-0.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ab2943be7c652f09638800905ee1bab2c544e537edb57d527997a24c13dc1455", size = 257393, upload-time = "2025-10-08T19:48:31.567Z" }, + { url = "https://files.pythonhosted.org/packages/08/02/87b25304249a35c0915d236575bc3574a323f60b47939a2262b77632a3ee/propcache-0.4.1-cp314-cp314t-win32.whl", hash = "sha256:05674a162469f31358c30bcaa8883cb7829fa3110bf9c0991fe27d7896c42d85", size = 42546, upload-time = "2025-10-08T19:48:32.872Z" }, + { url = "https://files.pythonhosted.org/packages/cb/ef/3c6ecf8b317aa982f309835e8f96987466123c6e596646d4e6a1dfcd080f/propcache-0.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:990f6b3e2a27d683cb7602ed6c86f15ee6b43b1194736f9baaeb93d0016633b1", size = 46259, upload-time = "2025-10-08T19:48:34.226Z" }, + { url = "https://files.pythonhosted.org/packages/c4/2d/346e946d4951f37eca1e4f55be0f0174c52cd70720f84029b02f296f4a38/propcache-0.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ecef2343af4cc68e05131e45024ba34f6095821988a9d0a02aa7c73fcc448aa9", size = 40428, upload-time = "2025-10-08T19:48:35.441Z" }, + { url = "https://files.pythonhosted.org/packages/9b/01/0ebaec9003f5d619a7475165961f8e3083cf8644d704b60395df3601632d/propcache-0.4.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:3d233076ccf9e450c8b3bc6720af226b898ef5d051a2d145f7d765e6e9f9bcff", size = 80277, upload-time = "2025-10-08T19:48:36.647Z" }, + { url = "https://files.pythonhosted.org/packages/34/58/04af97ac586b4ef6b9026c3fd36ee7798b737a832f5d3440a4280dcebd3a/propcache-0.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:357f5bb5c377a82e105e44bd3d52ba22b616f7b9773714bff93573988ef0a5fb", size = 45865, upload-time = "2025-10-08T19:48:37.859Z" }, + { url = "https://files.pythonhosted.org/packages/7c/19/b65d98ae21384518b291d9939e24a8aeac4fdb5101b732576f8f7540e834/propcache-0.4.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:cbc3b6dfc728105b2a57c06791eb07a94229202ea75c59db644d7d496b698cac", size = 47636, upload-time = "2025-10-08T19:48:39.038Z" }, + { url = "https://files.pythonhosted.org/packages/b3/0f/317048c6d91c356c7154dca5af019e6effeb7ee15fa6a6db327cc19e12b4/propcache-0.4.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:182b51b421f0501952d938dc0b0eb45246a5b5153c50d42b495ad5fb7517c888", size = 201126, upload-time = "2025-10-08T19:48:40.774Z" }, + { url = "https://files.pythonhosted.org/packages/71/69/0b2a7a5a6ee83292b4b997dbd80549d8ce7d40b6397c1646c0d9495f5a85/propcache-0.4.1-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4b536b39c5199b96fc6245eb5fb796c497381d3942f169e44e8e392b29c9ebcc", size = 209837, upload-time = "2025-10-08T19:48:42.167Z" }, + { url = "https://files.pythonhosted.org/packages/a5/92/c699ac495a6698df6e497fc2de27af4b6ace10d8e76528357ce153722e45/propcache-0.4.1-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:db65d2af507bbfbdcedb254a11149f894169d90488dd3e7190f7cdcb2d6cd57a", size = 215578, upload-time = "2025-10-08T19:48:43.56Z" }, + { url = "https://files.pythonhosted.org/packages/b3/ee/14de81c5eb02c0ee4f500b4e39c4e1bd0677c06e72379e6ab18923c773fc/propcache-0.4.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fd2dbc472da1f772a4dae4fa24be938a6c544671a912e30529984dd80400cd88", size = 197187, upload-time = "2025-10-08T19:48:45.309Z" }, + { url = "https://files.pythonhosted.org/packages/1d/94/48dce9aaa6d8dd5a0859bad75158ec522546d4ac23f8e2f05fac469477dd/propcache-0.4.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:daede9cd44e0f8bdd9e6cc9a607fc81feb80fae7a5fc6cecaff0e0bb32e42d00", size = 193478, upload-time = "2025-10-08T19:48:47.743Z" }, + { url = "https://files.pythonhosted.org/packages/60/b5/0516b563e801e1ace212afde869a0596a0d7115eec0b12d296d75633fb29/propcache-0.4.1-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:71b749281b816793678ae7f3d0d84bd36e694953822eaad408d682efc5ca18e0", size = 190650, upload-time = "2025-10-08T19:48:49.373Z" }, + { url = "https://files.pythonhosted.org/packages/24/89/e0f7d4a5978cd56f8cd67735f74052f257dc471ec901694e430f0d1572fe/propcache-0.4.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:0002004213ee1f36cfb3f9a42b5066100c44276b9b72b4e1504cddd3d692e86e", size = 200251, upload-time = "2025-10-08T19:48:51.4Z" }, + { url = "https://files.pythonhosted.org/packages/06/7d/a1fac863d473876ed4406c914f2e14aa82d2f10dd207c9e16fc383cc5a24/propcache-0.4.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:fe49d0a85038f36ba9e3ffafa1103e61170b28e95b16622e11be0a0ea07c6781", size = 200919, upload-time = "2025-10-08T19:48:53.227Z" }, + { url = "https://files.pythonhosted.org/packages/c3/4e/f86a256ff24944cf5743e4e6c6994e3526f6acfcfb55e21694c2424f758c/propcache-0.4.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:99d43339c83aaf4d32bda60928231848eee470c6bda8d02599cc4cebe872d183", size = 193211, upload-time = "2025-10-08T19:48:55.027Z" }, + { url = "https://files.pythonhosted.org/packages/6e/3f/3fbad5f4356b068f1b047d300a6ff2c66614d7030f078cd50be3fec04228/propcache-0.4.1-cp39-cp39-win32.whl", hash = "sha256:a129e76735bc792794d5177069691c3217898b9f5cee2b2661471e52ffe13f19", size = 38314, upload-time = "2025-10-08T19:48:56.792Z" }, + { url = "https://files.pythonhosted.org/packages/a4/45/d78d136c3a3d215677abb886785aae744da2c3005bcb99e58640c56529b1/propcache-0.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:948dab269721ae9a87fd16c514a0a2c2a1bdb23a9a61b969b0f9d9ee2968546f", size = 41912, upload-time = "2025-10-08T19:48:57.995Z" }, + { url = "https://files.pythonhosted.org/packages/fc/2a/b0632941f25139f4e58450b307242951f7c2717a5704977c6d5323a800af/propcache-0.4.1-cp39-cp39-win_arm64.whl", hash = "sha256:5fd37c406dd6dc85aa743e214cef35dc54bbdd1419baac4f6ae5e5b1a2976938", size = 38450, upload-time = "2025-10-08T19:48:59.349Z" }, + { url = "https://files.pythonhosted.org/packages/5b/5a/bc7b4a4ef808fa59a816c17b20c4bef6884daebbdf627ff2a161da67da19/propcache-0.4.1-py3-none-any.whl", hash = "sha256:af2a6052aeb6cf17d3e46ee169099044fd8224cbaf75c76a2ef596e8163e2237", size = 13305, upload-time = "2025-10-08T19:49:00.792Z" }, +] + +[[package]] +name = "pydantic" +version = "1.10.24" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", + "python_full_version < '3.10'", +] +dependencies = [ + { name = "typing-extensions", marker = "extra == 'group-9-stagehand-pydantic-v1'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ae/8d/7b346ed940c3e0f9eee7db9be37915a6dac0d9535d736e2ca47a81a066f3/pydantic-1.10.24.tar.gz", hash = "sha256:7e6d1af1bd3d2312079f28c9baf2aafb4a452a06b50717526e5ac562e37baa53", size = 357314, upload-time = "2025-09-25T01:36:33.065Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f9/6e/71eb0c860bf888e73243fbc22be79c47e68180b65b33036efb5a1f1085de/pydantic-1.10.24-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:eef07ea2fba12f9188cfa2c50cb3eaa6516b56c33e2a8cc3cd288b4190ee6c0c", size = 2494239, upload-time = "2025-09-25T01:35:02.451Z" }, + { url = "https://files.pythonhosted.org/packages/fd/1f/a2c09049c2ec33b88b111aa99e4bbfe9e821914dcf2ce662e00fa1423fa8/pydantic-1.10.24-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5a42033fac69b9f1f867ecc3a2159f0e94dceb1abfc509ad57e9e88d49774683", size = 2302370, upload-time = "2025-09-25T01:35:05.386Z" }, + { url = "https://files.pythonhosted.org/packages/70/73/fde9af3a76cc5714880828eee50c0f7f1b263d2c77a74c65ba19325b4706/pydantic-1.10.24-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c626596c1b95dc6d45f7129f10b6743fbb50f29d942d25a22b2ceead670c067d", size = 2960499, upload-time = "2025-09-25T01:35:07.243Z" }, + { url = "https://files.pythonhosted.org/packages/1d/d5/9fd98afa478020e9bad54a9ec6e42ba71f8a1a7f6df4d12ce5be76b0a96a/pydantic-1.10.24-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8057172868b0d98f95e6fcddcc5f75d01570e85c6308702dd2c50ea673bc197b", size = 3031125, upload-time = "2025-09-25T01:35:09.048Z" }, + { url = "https://files.pythonhosted.org/packages/1c/99/2fc6df8644c096dc6e3347e1793868a758df874eaf5ba52ca8b5a80e42d8/pydantic-1.10.24-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:82f951210ebcdb778b1d93075af43adcd04e9ebfd4f44b1baa8eeb21fbd71e36", size = 3099888, upload-time = "2025-09-25T01:35:10.894Z" }, + { url = "https://files.pythonhosted.org/packages/71/71/2f4c115951140f525136089da491b0bb4b7d24de8d697913afedde3f326c/pydantic-1.10.24-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b66e4892d8ae005f436a5c5f1519ecf837574d8414b1c93860fb3c13943d9b37", size = 3038385, upload-time = "2025-09-25T01:35:12.744Z" }, + { url = "https://files.pythonhosted.org/packages/ec/6d/d3893a9b8479a0ea9357ba4a1eb84a5776a80705b5409bda4ad9e7ca0804/pydantic-1.10.24-cp310-cp310-win_amd64.whl", hash = "sha256:50d9f8a207c07f347d4b34806dc576872000d9a60fd481ed9eb78ea8512e0666", size = 2093504, upload-time = "2025-09-25T01:35:14.439Z" }, + { url = "https://files.pythonhosted.org/packages/bd/b5/1b49b94e99ae4cad5f034c4b33e9ab481e53238fb55b59ffed5c6e6ee4cf/pydantic-1.10.24-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:70152291488f8d2bbcf2027b5c28c27724c78a7949c91b466d28ad75d6d12702", size = 2526778, upload-time = "2025-09-25T01:35:16.448Z" }, + { url = "https://files.pythonhosted.org/packages/87/d8/63fb1850ca93511b324d709f1c5bd31131039f9b93d0bc2ae210285db6d1/pydantic-1.10.24-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:956b30638272c51c85caaff76851b60db4b339022c0ee6eca677c41e3646255b", size = 2307760, upload-time = "2025-09-25T01:35:18.234Z" }, + { url = "https://files.pythonhosted.org/packages/2a/b8/428453ce573b8898afaf39a5ce32f7dbacf54f8aad9ce9c0abf19a1cdb2c/pydantic-1.10.24-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bed9d6eea5fabbc6978c42e947190c7bd628ddaff3b56fc963fe696c3710ccd6", size = 2902586, upload-time = "2025-09-25T01:35:20.118Z" }, + { url = "https://files.pythonhosted.org/packages/96/e0/68b5eb3c26b5e7136a8946f00f6d2eb8ef2fde530fcf6b491c66e3989d0d/pydantic-1.10.24-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:af8e2b3648128b8cadb1a71e2f8092a6f42d4ca123fad7a8d7ce6db8938b1db3", size = 2976378, upload-time = "2025-09-25T01:35:22.077Z" }, + { url = "https://files.pythonhosted.org/packages/93/8c/2c6c46b7bc265ba35bad019c63f77d9ef44fabc026353768d7e6ea16dd51/pydantic-1.10.24-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:076fff9da02ca716e4c8299c68512fdfbeac32fdefc9c160e6f80bdadca0993d", size = 3063515, upload-time = "2025-09-25T01:35:24.048Z" }, + { url = "https://files.pythonhosted.org/packages/84/be/a051e26eff43b6af69f968c1085cdf9069628a7c3614a9836d3ce71327e4/pydantic-1.10.24-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:8f2447ca88a7e14fd4d268857521fb37535c53a367b594fa2d7c2551af905993", size = 2988590, upload-time = "2025-09-25T01:35:25.794Z" }, + { url = "https://files.pythonhosted.org/packages/da/d8/f1aca10d538a0f18d2c99f7e84d3bb5c4abb6bd499272d6c4fc21f39af30/pydantic-1.10.24-cp311-cp311-win_amd64.whl", hash = "sha256:58d42a7c344882c00e3bb7c6c8c6f62db2e3aafa671f307271c45ad96e8ccf7a", size = 2096524, upload-time = "2025-09-25T01:35:27.367Z" }, + { url = "https://files.pythonhosted.org/packages/79/4b/73b59168d0babc14fb40b56795bde269d15709ef33de888e12e4f0add5ea/pydantic-1.10.24-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:17e7610119483f03954569c18d4de16f4e92f1585f20975414033ac2d4a96624", size = 2533707, upload-time = "2025-09-25T01:35:28.953Z" }, + { url = "https://files.pythonhosted.org/packages/6d/36/18e6f421a23ddceecfc5d3800d0e86af05e85574aa9e88cc9e29222db066/pydantic-1.10.24-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e24435a9970dcb2b35648f2cf57505d4bd414fcca1a404c82e28d948183fe0a6", size = 2322935, upload-time = "2025-09-25T01:35:30.838Z" }, + { url = "https://files.pythonhosted.org/packages/05/14/4d3fc4bea30add2f2f3c287e931b276f7e304bcb322fe5b2c05a76ccdee7/pydantic-1.10.24-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a9e92b9c78d7f3cfa085c21c110e7000894446e24a836d006aabfc6ae3f1813", size = 2779568, upload-time = "2025-09-25T01:35:32.309Z" }, + { url = "https://files.pythonhosted.org/packages/15/32/5349a7b6675d4384f07f9d461d8230de877b2b913529aa20e659c84bab07/pydantic-1.10.24-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ef14dfa7c98b314a3e449e92df6f1479cafe74c626952f353ff0176b075070de", size = 2829163, upload-time = "2025-09-25T01:35:34.294Z" }, + { url = "https://files.pythonhosted.org/packages/f0/00/4d93755e279e8975f7f33adc0af0e9d9aa0db58bcd9c807227d65b396311/pydantic-1.10.24-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52219b4e70c1db185cfd103a804e416384e1c8950168a2d4f385664c7c35d21a", size = 2912003, upload-time = "2025-09-25T01:35:35.935Z" }, + { url = "https://files.pythonhosted.org/packages/db/0c/c839c2a9cf14185c7b5dcc0959d3c3d4a00da400fe02565abf04a7dff6e0/pydantic-1.10.24-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5ce0986799248082e9a5a026c9b5d2f9fa2e24d2afb9b0eace9104334a58fdc1", size = 2859825, upload-time = "2025-09-25T01:35:37.657Z" }, + { url = "https://files.pythonhosted.org/packages/cb/0c/f0b8e35033322c176574d6f431455c8d6e3f63811a2c5a00c96b2b97a393/pydantic-1.10.24-cp312-cp312-win_amd64.whl", hash = "sha256:874a78e4ed821258295a472e325eee7de3d91ba7a61d0639ce1b0367a3c63d4c", size = 1969911, upload-time = "2025-09-25T01:35:39.479Z" }, + { url = "https://files.pythonhosted.org/packages/bd/56/9168c282af2bb8bdb102981a9ff0ed41ab4d3735a52b732b2d2ad0e14018/pydantic-1.10.24-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:265788a1120285c4955f8b3d52b3ea6a52c7a74db097c4c13a4d3567f0c6df3c", size = 2589497, upload-time = "2025-09-25T01:35:41.141Z" }, + { url = "https://files.pythonhosted.org/packages/9c/eb/eb4b0e2988a2889a1905c3196f859509e62c208830889d2382928b92fdd2/pydantic-1.10.24-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d255bebd927e5f1e026b32605684f7b6fc36a13e62b07cb97b29027b91657def", size = 2351231, upload-time = "2025-09-25T01:35:43.221Z" }, + { url = "https://files.pythonhosted.org/packages/1e/be/7451b633ffdc2d28de582a339af2275c3ffcca789dda97d8ac9133f0c616/pydantic-1.10.24-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d6e45dbc79a44e34c2c83ef1fcb56ff663040474dcf4dfc452db24a1de0f7574", size = 2762972, upload-time = "2025-09-25T01:35:45.304Z" }, + { url = "https://files.pythonhosted.org/packages/9e/fb/5de3cfde0b808f2fa0538ec1f1c186f44d905ecbcc96ba22e2cac1f30b23/pydantic-1.10.24-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:af31565b12a7db5bfa5fe8c3a4f8fda4d32f5c2929998b1b241f1c22e9ab6e69", size = 2801015, upload-time = "2025-09-25T01:35:46.774Z" }, + { url = "https://files.pythonhosted.org/packages/2f/6a/9b6b51d19d1af57e8864caff08ce5e8554388b91dc41987ce49315bce3e1/pydantic-1.10.24-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:9c377fc30d9ca40dbff5fd79c5a5e1f0d6fff040fa47a18851bb6b0bd040a5d8", size = 2890844, upload-time = "2025-09-25T01:35:48.724Z" }, + { url = "https://files.pythonhosted.org/packages/27/ca/1ab6b16bd792c8a1fb54949d8b5eef8032d672932ca4afc3048e4febfcdc/pydantic-1.10.24-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b644d6f14b2ce617d6def21622f9ba73961a16b7dffdba7f6692e2f66fa05d00", size = 2850844, upload-time = "2025-09-25T01:35:50.279Z" }, + { url = "https://files.pythonhosted.org/packages/86/5f/fcc5635818113858a6b37099fed6b860a15b27bb1d0fb270ceb50d0a91b6/pydantic-1.10.24-cp313-cp313-win_amd64.whl", hash = "sha256:0cbbf306124ae41cc153fdc2559b37faa1bec9a23ef7b082c1756d1315ceffe6", size = 1971713, upload-time = "2025-09-25T01:35:52.027Z" }, + { url = "https://files.pythonhosted.org/packages/a9/29/62dd3ffcf7d003f53e834942e9651c2ddd9dc6fb59e6619317e0ed37cf6b/pydantic-1.10.24-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:25fb9a69a21d711deb5acefdab9ff8fb49e6cc77fdd46d38217d433bff2e3de2", size = 2504290, upload-time = "2025-09-25T01:36:16.661Z" }, + { url = "https://files.pythonhosted.org/packages/f2/83/ef9c4be8e7fc96f52320296aed34f7cbe50fa0219833cc2756e611b644f2/pydantic-1.10.24-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6af36a8fb3072526b5b38d3f341b12d8f423188e7d185f130c0079fe02cdec7f", size = 2311007, upload-time = "2025-09-25T01:36:18.75Z" }, + { url = "https://files.pythonhosted.org/packages/1c/b7/ec7da8fbaac8c8100b05301a81fac6b2b7446961edb91bbef4b564834abf/pydantic-1.10.24-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5fc35569dfd15d3b3fc06a22abee0a45fdde0784be644e650a8769cd0b2abd94", size = 2968514, upload-time = "2025-09-25T01:36:20.511Z" }, + { url = "https://files.pythonhosted.org/packages/49/84/9e218a35008fbc32dac2974a35a4bd88d7deb0f5b572cf46ccf003a06310/pydantic-1.10.24-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fac7fbcb65171959973f3136d0792c3d1668bc01fd414738f0898b01f692f1b4", size = 3039539, upload-time = "2025-09-25T01:36:24.359Z" }, + { url = "https://files.pythonhosted.org/packages/b0/2f/b13a8c2d641e3af3fbba136202a9808025ee7cde4b1326ce1aabd1c79d51/pydantic-1.10.24-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:fc3f4a6544517380658b63b144c7d43d5276a343012913b7e5d18d9fba2f12bb", size = 3108949, upload-time = "2025-09-25T01:36:26.138Z" }, + { url = "https://files.pythonhosted.org/packages/1f/57/dccbf080b35b9797f4d477f4c59935e39e4493cd507f31b5ca5ee49c930d/pydantic-1.10.24-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:415c638ca5fd57b915a62dd38c18c8e0afe5adf5527be6f8ce16b4636b616816", size = 3049395, upload-time = "2025-09-25T01:36:27.782Z" }, + { url = "https://files.pythonhosted.org/packages/a2/ff/2a25855a1495fcbe1d3b8c782276994575e98ce2218dbf57c1f2eee7c894/pydantic-1.10.24-cp39-cp39-win_amd64.whl", hash = "sha256:a5bf94042efbc6ab56b18a5921f426ebbeefc04f554a911d76029e7be9057d01", size = 2100530, upload-time = "2025-09-25T01:36:29.932Z" }, + { url = "https://files.pythonhosted.org/packages/46/7f/a168d7077f85f85128aa5636abf13c804c06235c786f1881e659703899a4/pydantic-1.10.24-py3-none-any.whl", hash = "sha256:093768eba26db55a88b12f3073017e3fdee319ef60d3aef5c6c04a4e484db193", size = 166727, upload-time = "2025-09-25T01:36:31.732Z" }, +] + +[[package]] +name = "pydantic" +version = "2.12.5" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and extra != 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2'", + "python_full_version >= '3.10' and python_full_version < '3.14' and extra != 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2'", + "python_full_version < '3.10' and extra != 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2'", + "python_full_version >= '3.10' and extra != 'group-9-stagehand-pydantic-v1' and extra != 'group-9-stagehand-pydantic-v2'", + "python_full_version < '3.10' and extra != 'group-9-stagehand-pydantic-v1' and extra != 'group-9-stagehand-pydantic-v2'", +] +dependencies = [ + { name = "annotated-types", marker = "extra == 'group-9-stagehand-pydantic-v2' or extra != 'group-9-stagehand-pydantic-v1'" }, + { name = "pydantic-core", marker = "extra == 'group-9-stagehand-pydantic-v2' or extra != 'group-9-stagehand-pydantic-v1'" }, + { name = "typing-extensions", marker = "extra == 'group-9-stagehand-pydantic-v2' or extra != 'group-9-stagehand-pydantic-v1'" }, + { name = "typing-inspection", marker = "extra == 'group-9-stagehand-pydantic-v2' or extra != 'group-9-stagehand-pydantic-v1'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591, upload-time = "2025-11-26T15:11:46.471Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.41.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "extra == 'group-9-stagehand-pydantic-v2' or extra != 'group-9-stagehand-pydantic-v1'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c6/90/32c9941e728d564b411d574d8ee0cf09b12ec978cb22b294995bae5549a5/pydantic_core-2.41.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:77b63866ca88d804225eaa4af3e664c5faf3568cea95360d21f4725ab6e07146", size = 2107298, upload-time = "2025-11-04T13:39:04.116Z" }, + { url = "https://files.pythonhosted.org/packages/fb/a8/61c96a77fe28993d9a6fb0f4127e05430a267b235a124545d79fea46dd65/pydantic_core-2.41.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dfa8a0c812ac681395907e71e1274819dec685fec28273a28905df579ef137e2", size = 1901475, upload-time = "2025-11-04T13:39:06.055Z" }, + { url = "https://files.pythonhosted.org/packages/5d/b6/338abf60225acc18cdc08b4faef592d0310923d19a87fba1faf05af5346e/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5921a4d3ca3aee735d9fd163808f5e8dd6c6972101e4adbda9a4667908849b97", size = 1918815, upload-time = "2025-11-04T13:39:10.41Z" }, + { url = "https://files.pythonhosted.org/packages/d1/1c/2ed0433e682983d8e8cba9c8d8ef274d4791ec6a6f24c58935b90e780e0a/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e25c479382d26a2a41b7ebea1043564a937db462816ea07afa8a44c0866d52f9", size = 2065567, upload-time = "2025-11-04T13:39:12.244Z" }, + { url = "https://files.pythonhosted.org/packages/b3/24/cf84974ee7d6eae06b9e63289b7b8f6549d416b5c199ca2d7ce13bbcf619/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f547144f2966e1e16ae626d8ce72b4cfa0caedc7fa28052001c94fb2fcaa1c52", size = 2230442, upload-time = "2025-11-04T13:39:13.962Z" }, + { url = "https://files.pythonhosted.org/packages/fd/21/4e287865504b3edc0136c89c9c09431be326168b1eb7841911cbc877a995/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f52298fbd394f9ed112d56f3d11aabd0d5bd27beb3084cc3d8ad069483b8941", size = 2350956, upload-time = "2025-11-04T13:39:15.889Z" }, + { url = "https://files.pythonhosted.org/packages/a8/76/7727ef2ffa4b62fcab916686a68a0426b9b790139720e1934e8ba797e238/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:100baa204bb412b74fe285fb0f3a385256dad1d1879f0a5cb1499ed2e83d132a", size = 2068253, upload-time = "2025-11-04T13:39:17.403Z" }, + { url = "https://files.pythonhosted.org/packages/d5/8c/a4abfc79604bcb4c748e18975c44f94f756f08fb04218d5cb87eb0d3a63e/pydantic_core-2.41.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:05a2c8852530ad2812cb7914dc61a1125dc4e06252ee98e5638a12da6cc6fb6c", size = 2177050, upload-time = "2025-11-04T13:39:19.351Z" }, + { url = "https://files.pythonhosted.org/packages/67/b1/de2e9a9a79b480f9cb0b6e8b6ba4c50b18d4e89852426364c66aa82bb7b3/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:29452c56df2ed968d18d7e21f4ab0ac55e71dc59524872f6fc57dcf4a3249ed2", size = 2147178, upload-time = "2025-11-04T13:39:21Z" }, + { url = "https://files.pythonhosted.org/packages/16/c1/dfb33f837a47b20417500efaa0378adc6635b3c79e8369ff7a03c494b4ac/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:d5160812ea7a8a2ffbe233d8da666880cad0cbaf5d4de74ae15c313213d62556", size = 2341833, upload-time = "2025-11-04T13:39:22.606Z" }, + { url = "https://files.pythonhosted.org/packages/47/36/00f398642a0f4b815a9a558c4f1dca1b4020a7d49562807d7bc9ff279a6c/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:df3959765b553b9440adfd3c795617c352154e497a4eaf3752555cfb5da8fc49", size = 2321156, upload-time = "2025-11-04T13:39:25.843Z" }, + { url = "https://files.pythonhosted.org/packages/7e/70/cad3acd89fde2010807354d978725ae111ddf6d0ea46d1ea1775b5c1bd0c/pydantic_core-2.41.5-cp310-cp310-win32.whl", hash = "sha256:1f8d33a7f4d5a7889e60dc39856d76d09333d8a6ed0f5f1190635cbec70ec4ba", size = 1989378, upload-time = "2025-11-04T13:39:27.92Z" }, + { url = "https://files.pythonhosted.org/packages/76/92/d338652464c6c367e5608e4488201702cd1cbb0f33f7b6a85a60fe5f3720/pydantic_core-2.41.5-cp310-cp310-win_amd64.whl", hash = "sha256:62de39db01b8d593e45871af2af9e497295db8d73b085f6bfd0b18c83c70a8f9", size = 2013622, upload-time = "2025-11-04T13:39:29.848Z" }, + { url = "https://files.pythonhosted.org/packages/e8/72/74a989dd9f2084b3d9530b0915fdda64ac48831c30dbf7c72a41a5232db8/pydantic_core-2.41.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6", size = 2105873, upload-time = "2025-11-04T13:39:31.373Z" }, + { url = "https://files.pythonhosted.org/packages/12/44/37e403fd9455708b3b942949e1d7febc02167662bf1a7da5b78ee1ea2842/pydantic_core-2.41.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b", size = 1899826, upload-time = "2025-11-04T13:39:32.897Z" }, + { url = "https://files.pythonhosted.org/packages/33/7f/1d5cab3ccf44c1935a359d51a8a2a9e1a654b744b5e7f80d41b88d501eec/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a", size = 1917869, upload-time = "2025-11-04T13:39:34.469Z" }, + { url = "https://files.pythonhosted.org/packages/6e/6a/30d94a9674a7fe4f4744052ed6c5e083424510be1e93da5bc47569d11810/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8", size = 2063890, upload-time = "2025-11-04T13:39:36.053Z" }, + { url = "https://files.pythonhosted.org/packages/50/be/76e5d46203fcb2750e542f32e6c371ffa9b8ad17364cf94bb0818dbfb50c/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e", size = 2229740, upload-time = "2025-11-04T13:39:37.753Z" }, + { url = "https://files.pythonhosted.org/packages/d3/ee/fed784df0144793489f87db310a6bbf8118d7b630ed07aa180d6067e653a/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1", size = 2350021, upload-time = "2025-11-04T13:39:40.94Z" }, + { url = "https://files.pythonhosted.org/packages/c8/be/8fed28dd0a180dca19e72c233cbf58efa36df055e5b9d90d64fd1740b828/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b", size = 2066378, upload-time = "2025-11-04T13:39:42.523Z" }, + { url = "https://files.pythonhosted.org/packages/b0/3b/698cf8ae1d536a010e05121b4958b1257f0b5522085e335360e53a6b1c8b/pydantic_core-2.41.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c1df3d34aced70add6f867a8cf413e299177e0c22660cc767218373d0779487b", size = 2175761, upload-time = "2025-11-04T13:39:44.553Z" }, + { url = "https://files.pythonhosted.org/packages/b8/ba/15d537423939553116dea94ce02f9c31be0fa9d0b806d427e0308ec17145/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284", size = 2146303, upload-time = "2025-11-04T13:39:46.238Z" }, + { url = "https://files.pythonhosted.org/packages/58/7f/0de669bf37d206723795f9c90c82966726a2ab06c336deba4735b55af431/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594", size = 2340355, upload-time = "2025-11-04T13:39:48.002Z" }, + { url = "https://files.pythonhosted.org/packages/e5/de/e7482c435b83d7e3c3ee5ee4451f6e8973cff0eb6007d2872ce6383f6398/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e", size = 2319875, upload-time = "2025-11-04T13:39:49.705Z" }, + { url = "https://files.pythonhosted.org/packages/fe/e6/8c9e81bb6dd7560e33b9053351c29f30c8194b72f2d6932888581f503482/pydantic_core-2.41.5-cp311-cp311-win32.whl", hash = "sha256:2c010c6ded393148374c0f6f0bf89d206bf3217f201faa0635dcd56bd1520f6b", size = 1987549, upload-time = "2025-11-04T13:39:51.842Z" }, + { url = "https://files.pythonhosted.org/packages/11/66/f14d1d978ea94d1bc21fc98fcf570f9542fe55bfcc40269d4e1a21c19bf7/pydantic_core-2.41.5-cp311-cp311-win_amd64.whl", hash = "sha256:76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe", size = 2011305, upload-time = "2025-11-04T13:39:53.485Z" }, + { url = "https://files.pythonhosted.org/packages/56/d8/0e271434e8efd03186c5386671328154ee349ff0354d83c74f5caaf096ed/pydantic_core-2.41.5-cp311-cp311-win_arm64.whl", hash = "sha256:4bc36bbc0b7584de96561184ad7f012478987882ebf9f9c389b23f432ea3d90f", size = 1972902, upload-time = "2025-11-04T13:39:56.488Z" }, + { url = "https://files.pythonhosted.org/packages/5f/5d/5f6c63eebb5afee93bcaae4ce9a898f3373ca23df3ccaef086d0233a35a7/pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7", size = 2110990, upload-time = "2025-11-04T13:39:58.079Z" }, + { url = "https://files.pythonhosted.org/packages/aa/32/9c2e8ccb57c01111e0fd091f236c7b371c1bccea0fa85247ac55b1e2b6b6/pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0", size = 1896003, upload-time = "2025-11-04T13:39:59.956Z" }, + { url = "https://files.pythonhosted.org/packages/68/b8/a01b53cb0e59139fbc9e4fda3e9724ede8de279097179be4ff31f1abb65a/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", size = 1919200, upload-time = "2025-11-04T13:40:02.241Z" }, + { url = "https://files.pythonhosted.org/packages/38/de/8c36b5198a29bdaade07b5985e80a233a5ac27137846f3bc2d3b40a47360/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75", size = 2052578, upload-time = "2025-11-04T13:40:04.401Z" }, + { url = "https://files.pythonhosted.org/packages/00/b5/0e8e4b5b081eac6cb3dbb7e60a65907549a1ce035a724368c330112adfdd/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05", size = 2208504, upload-time = "2025-11-04T13:40:06.072Z" }, + { url = "https://files.pythonhosted.org/packages/77/56/87a61aad59c7c5b9dc8caad5a41a5545cba3810c3e828708b3d7404f6cef/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc", size = 2335816, upload-time = "2025-11-04T13:40:07.835Z" }, + { url = "https://files.pythonhosted.org/packages/0d/76/941cc9f73529988688a665a5c0ecff1112b3d95ab48f81db5f7606f522d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c", size = 2075366, upload-time = "2025-11-04T13:40:09.804Z" }, + { url = "https://files.pythonhosted.org/packages/d3/43/ebef01f69baa07a482844faaa0a591bad1ef129253ffd0cdaa9d8a7f72d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5", size = 2171698, upload-time = "2025-11-04T13:40:12.004Z" }, + { url = "https://files.pythonhosted.org/packages/b1/87/41f3202e4193e3bacfc2c065fab7706ebe81af46a83d3e27605029c1f5a6/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c", size = 2132603, upload-time = "2025-11-04T13:40:13.868Z" }, + { url = "https://files.pythonhosted.org/packages/49/7d/4c00df99cb12070b6bccdef4a195255e6020a550d572768d92cc54dba91a/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294", size = 2329591, upload-time = "2025-11-04T13:40:15.672Z" }, + { url = "https://files.pythonhosted.org/packages/cc/6a/ebf4b1d65d458f3cda6a7335d141305dfa19bdc61140a884d165a8a1bbc7/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1", size = 2319068, upload-time = "2025-11-04T13:40:17.532Z" }, + { url = "https://files.pythonhosted.org/packages/49/3b/774f2b5cd4192d5ab75870ce4381fd89cf218af999515baf07e7206753f0/pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d", size = 1985908, upload-time = "2025-11-04T13:40:19.309Z" }, + { url = "https://files.pythonhosted.org/packages/86/45/00173a033c801cacf67c190fef088789394feaf88a98a7035b0e40d53dc9/pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815", size = 2020145, upload-time = "2025-11-04T13:40:21.548Z" }, + { url = "https://files.pythonhosted.org/packages/f9/22/91fbc821fa6d261b376a3f73809f907cec5ca6025642c463d3488aad22fb/pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3", size = 1976179, upload-time = "2025-11-04T13:40:23.393Z" }, + { url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403, upload-time = "2025-11-04T13:40:25.248Z" }, + { url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206, upload-time = "2025-11-04T13:40:27.099Z" }, + { url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307, upload-time = "2025-11-04T13:40:29.806Z" }, + { url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258, upload-time = "2025-11-04T13:40:33.544Z" }, + { url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917, upload-time = "2025-11-04T13:40:35.479Z" }, + { url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186, upload-time = "2025-11-04T13:40:37.436Z" }, + { url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164, upload-time = "2025-11-04T13:40:40.289Z" }, + { url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146, upload-time = "2025-11-04T13:40:42.809Z" }, + { url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788, upload-time = "2025-11-04T13:40:44.752Z" }, + { url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133, upload-time = "2025-11-04T13:40:46.66Z" }, + { url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852, upload-time = "2025-11-04T13:40:48.575Z" }, + { url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679, upload-time = "2025-11-04T13:40:50.619Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766, upload-time = "2025-11-04T13:40:52.631Z" }, + { url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005, upload-time = "2025-11-04T13:40:54.734Z" }, + { url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622, upload-time = "2025-11-04T13:40:56.68Z" }, + { url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725, upload-time = "2025-11-04T13:40:58.807Z" }, + { url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040, upload-time = "2025-11-04T13:41:00.853Z" }, + { url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691, upload-time = "2025-11-04T13:41:03.504Z" }, + { url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897, upload-time = "2025-11-04T13:41:05.804Z" }, + { url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302, upload-time = "2025-11-04T13:41:07.809Z" }, + { url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877, upload-time = "2025-11-04T13:41:09.827Z" }, + { url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680, upload-time = "2025-11-04T13:41:12.379Z" }, + { url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960, upload-time = "2025-11-04T13:41:14.627Z" }, + { url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102, upload-time = "2025-11-04T13:41:16.868Z" }, + { url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039, upload-time = "2025-11-04T13:41:18.934Z" }, + { url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126, upload-time = "2025-11-04T13:41:21.418Z" }, + { url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489, upload-time = "2025-11-04T13:41:24.076Z" }, + { url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288, upload-time = "2025-11-04T13:41:26.33Z" }, + { url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255, upload-time = "2025-11-04T13:41:28.569Z" }, + { url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760, upload-time = "2025-11-04T13:41:31.055Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092, upload-time = "2025-11-04T13:41:33.21Z" }, + { url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385, upload-time = "2025-11-04T13:41:35.508Z" }, + { url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832, upload-time = "2025-11-04T13:41:37.732Z" }, + { url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585, upload-time = "2025-11-04T13:41:40Z" }, + { url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078, upload-time = "2025-11-04T13:41:42.323Z" }, + { url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914, upload-time = "2025-11-04T13:41:45.221Z" }, + { url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560, upload-time = "2025-11-04T13:41:47.474Z" }, + { url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244, upload-time = "2025-11-04T13:41:49.992Z" }, + { url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955, upload-time = "2025-11-04T13:41:54.079Z" }, + { url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906, upload-time = "2025-11-04T13:41:56.606Z" }, + { url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607, upload-time = "2025-11-04T13:41:58.889Z" }, + { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" }, + { url = "https://files.pythonhosted.org/packages/54/db/160dffb57ed9a3705c4cbcbff0ac03bdae45f1ca7d58ab74645550df3fbd/pydantic_core-2.41.5-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:8bfeaf8735be79f225f3fefab7f941c712aaca36f1128c9d7e2352ee1aa87bdf", size = 2107999, upload-time = "2025-11-04T13:42:03.885Z" }, + { url = "https://files.pythonhosted.org/packages/a3/7d/88e7de946f60d9263cc84819f32513520b85c0f8322f9b8f6e4afc938383/pydantic_core-2.41.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:346285d28e4c8017da95144c7f3acd42740d637ff41946af5ce6e5e420502dd5", size = 1929745, upload-time = "2025-11-04T13:42:06.075Z" }, + { url = "https://files.pythonhosted.org/packages/d5/c2/aef51e5b283780e85e99ff19db0f05842d2d4a8a8cd15e63b0280029b08f/pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a75dafbf87d6276ddc5b2bf6fae5254e3d0876b626eb24969a574fff9149ee5d", size = 1920220, upload-time = "2025-11-04T13:42:08.457Z" }, + { url = "https://files.pythonhosted.org/packages/c7/97/492ab10f9ac8695cd76b2fdb24e9e61f394051df71594e9bcc891c9f586e/pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7b93a4d08587e2b7e7882de461e82b6ed76d9026ce91ca7915e740ecc7855f60", size = 2067296, upload-time = "2025-11-04T13:42:10.817Z" }, + { url = "https://files.pythonhosted.org/packages/ec/23/984149650e5269c59a2a4c41d234a9570adc68ab29981825cfaf4cfad8f4/pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e8465ab91a4bd96d36dde3263f06caa6a8a6019e4113f24dc753d79a8b3a3f82", size = 2231548, upload-time = "2025-11-04T13:42:13.843Z" }, + { url = "https://files.pythonhosted.org/packages/71/0c/85bcbb885b9732c28bec67a222dbed5ed2d77baee1f8bba2002e8cd00c5c/pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:299e0a22e7ae2b85c1a57f104538b2656e8ab1873511fd718a1c1c6f149b77b5", size = 2362571, upload-time = "2025-11-04T13:42:16.208Z" }, + { url = "https://files.pythonhosted.org/packages/c0/4a/412d2048be12c334003e9b823a3fa3d038e46cc2d64dd8aab50b31b65499/pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:707625ef0983fcfb461acfaf14de2067c5942c6bb0f3b4c99158bed6fedd3cf3", size = 2068175, upload-time = "2025-11-04T13:42:18.911Z" }, + { url = "https://files.pythonhosted.org/packages/73/f4/c58b6a776b502d0a5540ad02e232514285513572060f0d78f7832ca3c98b/pydantic_core-2.41.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f41eb9797986d6ebac5e8edff36d5cef9de40def462311b3eb3eeded1431e425", size = 2177203, upload-time = "2025-11-04T13:42:22.578Z" }, + { url = "https://files.pythonhosted.org/packages/ed/ae/f06ea4c7e7a9eead3d165e7623cd2ea0cb788e277e4f935af63fc98fa4e6/pydantic_core-2.41.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0384e2e1021894b1ff5a786dbf94771e2986ebe2869533874d7e43bc79c6f504", size = 2148191, upload-time = "2025-11-04T13:42:24.89Z" }, + { url = "https://files.pythonhosted.org/packages/c1/57/25a11dcdc656bf5f8b05902c3c2934ac3ea296257cc4a3f79a6319e61856/pydantic_core-2.41.5-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:f0cd744688278965817fd0839c4a4116add48d23890d468bc436f78beb28abf5", size = 2343907, upload-time = "2025-11-04T13:42:27.683Z" }, + { url = "https://files.pythonhosted.org/packages/96/82/e33d5f4933d7a03327c0c43c65d575e5919d4974ffc026bc917a5f7b9f61/pydantic_core-2.41.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:753e230374206729bf0a807954bcc6c150d3743928a73faffee51ac6557a03c3", size = 2322174, upload-time = "2025-11-04T13:42:30.776Z" }, + { url = "https://files.pythonhosted.org/packages/81/45/4091be67ce9f469e81656f880f3506f6a5624121ec5eb3eab37d7581897d/pydantic_core-2.41.5-cp39-cp39-win32.whl", hash = "sha256:873e0d5b4fb9b89ef7c2d2a963ea7d02879d9da0da8d9d4933dee8ee86a8b460", size = 1990353, upload-time = "2025-11-04T13:42:33.111Z" }, + { url = "https://files.pythonhosted.org/packages/44/8a/a98aede18db6e9cd5d66bcacd8a409fcf8134204cdede2e7de35c5a2c5ef/pydantic_core-2.41.5-cp39-cp39-win_amd64.whl", hash = "sha256:e4f4a984405e91527a0d62649ee21138f8e3d0ef103be488c1dc11a80d7f184b", size = 2015698, upload-time = "2025-11-04T13:42:35.484Z" }, + { url = "https://files.pythonhosted.org/packages/11/72/90fda5ee3b97e51c494938a4a44c3a35a9c96c19bba12372fb9c634d6f57/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034", size = 2115441, upload-time = "2025-11-04T13:42:39.557Z" }, + { url = "https://files.pythonhosted.org/packages/1f/53/8942f884fa33f50794f119012dc6a1a02ac43a56407adaac20463df8e98f/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c", size = 1930291, upload-time = "2025-11-04T13:42:42.169Z" }, + { url = "https://files.pythonhosted.org/packages/79/c8/ecb9ed9cd942bce09fc888ee960b52654fbdbede4ba6c2d6e0d3b1d8b49c/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2", size = 1948632, upload-time = "2025-11-04T13:42:44.564Z" }, + { url = "https://files.pythonhosted.org/packages/2e/1b/687711069de7efa6af934e74f601e2a4307365e8fdc404703afc453eab26/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad", size = 2138905, upload-time = "2025-11-04T13:42:47.156Z" }, + { url = "https://files.pythonhosted.org/packages/09/32/59b0c7e63e277fa7911c2fc70ccfb45ce4b98991e7ef37110663437005af/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd", size = 2110495, upload-time = "2025-11-04T13:42:49.689Z" }, + { url = "https://files.pythonhosted.org/packages/aa/81/05e400037eaf55ad400bcd318c05bb345b57e708887f07ddb2d20e3f0e98/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", size = 1915388, upload-time = "2025-11-04T13:42:52.215Z" }, + { url = "https://files.pythonhosted.org/packages/6e/0d/e3549b2399f71d56476b77dbf3cf8937cec5cd70536bdc0e374a421d0599/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", size = 1942879, upload-time = "2025-11-04T13:42:56.483Z" }, + { url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017, upload-time = "2025-11-04T13:42:59.471Z" }, + { url = "https://files.pythonhosted.org/packages/e6/b0/1a2aa41e3b5a4ba11420aba2d091b2d17959c8d1519ece3627c371951e73/pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b5819cd790dbf0c5eb9f82c73c16b39a65dd6dd4d1439dcdea7816ec9adddab8", size = 2103351, upload-time = "2025-11-04T13:43:02.058Z" }, + { url = "https://files.pythonhosted.org/packages/a4/ee/31b1f0020baaf6d091c87900ae05c6aeae101fa4e188e1613c80e4f1ea31/pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5a4e67afbc95fa5c34cf27d9089bca7fcab4e51e57278d710320a70b956d1b9a", size = 1925363, upload-time = "2025-11-04T13:43:05.159Z" }, + { url = "https://files.pythonhosted.org/packages/e1/89/ab8e86208467e467a80deaca4e434adac37b10a9d134cd2f99b28a01e483/pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ece5c59f0ce7d001e017643d8d24da587ea1f74f6993467d85ae8a5ef9d4f42b", size = 2135615, upload-time = "2025-11-04T13:43:08.116Z" }, + { url = "https://files.pythonhosted.org/packages/99/0a/99a53d06dd0348b2008f2f30884b34719c323f16c3be4e6cc1203b74a91d/pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:16f80f7abe3351f8ea6858914ddc8c77e02578544a0ebc15b4c2e1a0e813b0b2", size = 2175369, upload-time = "2025-11-04T13:43:12.49Z" }, + { url = "https://files.pythonhosted.org/packages/6d/94/30ca3b73c6d485b9bb0bc66e611cff4a7138ff9736b7e66bcf0852151636/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:33cb885e759a705b426baada1fe68cbb0a2e68e34c5d0d0289a364cf01709093", size = 2144218, upload-time = "2025-11-04T13:43:15.431Z" }, + { url = "https://files.pythonhosted.org/packages/87/57/31b4f8e12680b739a91f472b5671294236b82586889ef764b5fbc6669238/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:c8d8b4eb992936023be7dee581270af5c6e0697a8559895f527f5b7105ecd36a", size = 2329951, upload-time = "2025-11-04T13:43:18.062Z" }, + { url = "https://files.pythonhosted.org/packages/7d/73/3c2c8edef77b8f7310e6fb012dbc4b8551386ed575b9eb6fb2506e28a7eb/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:242a206cd0318f95cd21bdacff3fcc3aab23e79bba5cac3db5a841c9ef9c6963", size = 2318428, upload-time = "2025-11-04T13:43:20.679Z" }, + { url = "https://files.pythonhosted.org/packages/2f/02/8559b1f26ee0d502c74f9cca5c0d2fd97e967e083e006bbbb4e97f3a043a/pydantic_core-2.41.5-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d3a978c4f57a597908b7e697229d996d77a6d3c94901e9edee593adada95ce1a", size = 2147009, upload-time = "2025-11-04T13:43:23.286Z" }, + { url = "https://files.pythonhosted.org/packages/5f/9b/1b3f0e9f9305839d7e84912f9e8bfbd191ed1b1ef48083609f0dabde978c/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26", size = 2101980, upload-time = "2025-11-04T13:43:25.97Z" }, + { url = "https://files.pythonhosted.org/packages/a4/ed/d71fefcb4263df0da6a85b5d8a7508360f2f2e9b3bf5814be9c8bccdccc1/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808", size = 1923865, upload-time = "2025-11-04T13:43:28.763Z" }, + { url = "https://files.pythonhosted.org/packages/ce/3a/626b38db460d675f873e4444b4bb030453bbe7b4ba55df821d026a0493c4/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc", size = 2134256, upload-time = "2025-11-04T13:43:31.71Z" }, + { url = "https://files.pythonhosted.org/packages/83/d9/8412d7f06f616bbc053d30cb4e5f76786af3221462ad5eee1f202021eb4e/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:287dad91cfb551c363dc62899a80e9e14da1f0e2b6ebde82c806612ca2a13ef1", size = 2174762, upload-time = "2025-11-04T13:43:34.744Z" }, + { url = "https://files.pythonhosted.org/packages/55/4c/162d906b8e3ba3a99354e20faa1b49a85206c47de97a639510a0e673f5da/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84", size = 2143141, upload-time = "2025-11-04T13:43:37.701Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f2/f11dd73284122713f5f89fc940f370d035fa8e1e078d446b3313955157fe/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770", size = 2330317, upload-time = "2025-11-04T13:43:40.406Z" }, + { url = "https://files.pythonhosted.org/packages/88/9d/b06ca6acfe4abb296110fb1273a4d848a0bfb2ff65f3ee92127b3244e16b/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f", size = 2316992, upload-time = "2025-11-04T13:43:43.602Z" }, + { url = "https://files.pythonhosted.org/packages/36/c7/cfc8e811f061c841d7990b0201912c3556bfeb99cdcb7ed24adc8d6f8704/pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51", size = 2145302, upload-time = "2025-11-04T13:43:46.64Z" }, +] + +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, +] + +[[package]] +name = "pyright" +version = "1.1.399" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nodeenv" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/db/9d/d91d5f6d26b2db95476fefc772e2b9a16d54c6bd0ea6bb5c1b6d635ab8b4/pyright-1.1.399.tar.gz", hash = "sha256:439035d707a36c3d1b443aec980bc37053fbda88158eded24b8eedcf1c7b7a1b", size = 3856954, upload-time = "2025-04-10T04:40:25.703Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2f/b5/380380c9e7a534cb1783c70c3e8ac6d1193c599650a55838d0557586796e/pyright-1.1.399-py3-none-any.whl", hash = "sha256:55f9a875ddf23c9698f24208c764465ffdfd38be6265f7faf9a176e1dc549f3b", size = 5592584, upload-time = "2025-04-10T04:40:23.502Z" }, +] + +[[package]] +name = "pytest" +version = "8.4.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "colorama", marker = "(python_full_version < '3.10' and sys_platform == 'win32') or (python_full_version >= '3.10' and extra == 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2') or (sys_platform != 'win32' and extra == 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2')" }, + { name = "exceptiongroup", marker = "python_full_version < '3.10' or (extra == 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2')" }, + { name = "iniconfig", version = "2.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10' or (extra == 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2')" }, + { name = "packaging", marker = "python_full_version < '3.10' or (extra == 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2')" }, + { name = "pluggy", marker = "python_full_version < '3.10' or (extra == 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2')" }, + { name = "pygments", marker = "python_full_version < '3.10' or (extra == 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2')" }, + { name = "tomli", marker = "python_full_version < '3.10' or (extra == 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" }, +] + +[[package]] +name = "pytest" +version = "9.0.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and extra != 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2'", + "python_full_version >= '3.10' and python_full_version < '3.14' and extra != 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2'", + "python_full_version >= '3.10' and extra == 'group-9-stagehand-pydantic-v1' and extra != 'group-9-stagehand-pydantic-v2'", + "python_full_version >= '3.10' and extra != 'group-9-stagehand-pydantic-v1' and extra != 'group-9-stagehand-pydantic-v2'", +] +dependencies = [ + { name = "colorama", marker = "(python_full_version >= '3.10' and sys_platform == 'win32') or (python_full_version < '3.10' and extra == 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2') or (sys_platform != 'win32' and extra == 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2')" }, + { name = "exceptiongroup", marker = "python_full_version == '3.10.*' or (extra == 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2')" }, + { name = "iniconfig", version = "2.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10' or (extra == 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2')" }, + { name = "packaging", marker = "python_full_version >= '3.10' or (extra == 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2')" }, + { name = "pluggy", marker = "python_full_version >= '3.10' or (extra == 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2')" }, + { name = "pygments", marker = "python_full_version >= '3.10' or (extra == 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2')" }, + { name = "tomli", marker = "python_full_version == '3.10.*' or (extra == 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/07/56/f013048ac4bc4c1d9be45afd4ab209ea62822fb1598f40687e6bf45dcea4/pytest-9.0.1.tar.gz", hash = "sha256:3e9c069ea73583e255c3b21cf46b8d3c56f6e3a1a8f6da94ccb0fcf57b9d73c8", size = 1564125, upload-time = "2025-11-12T13:05:09.333Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/8b/6300fb80f858cda1c51ffa17075df5d846757081d11ab4aa35cef9e6258b/pytest-9.0.1-py3-none-any.whl", hash = "sha256:67be0030d194df2dfa7b556f2e56fb3c3315bd5c8822c6951162b92b32ce7dad", size = 373668, upload-time = "2025-11-12T13:05:07.379Z" }, +] + +[[package]] +name = "pytest-asyncio" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "backports-asyncio-runner", marker = "python_full_version < '3.10' or (extra == 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2')" }, + { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10' or (extra == 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2')" }, + { name = "typing-extensions", marker = "python_full_version < '3.10' or (extra == 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/86/9e3c5f48f7b7b638b216e4b9e645f54d199d7abbbab7a64a13b4e12ba10f/pytest_asyncio-1.2.0.tar.gz", hash = "sha256:c609a64a2a8768462d0c99811ddb8bd2583c33fd33cf7f21af1c142e824ffb57", size = 50119, upload-time = "2025-09-12T07:33:53.816Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/93/2fa34714b7a4ae72f2f8dad66ba17dd9a2c793220719e736dda28b7aec27/pytest_asyncio-1.2.0-py3-none-any.whl", hash = "sha256:8e17ae5e46d8e7efe51ab6494dd2010f4ca8dae51652aa3c8d55acf50bfb2e99", size = 15095, upload-time = "2025-09-12T07:33:52.639Z" }, +] + +[[package]] +name = "pytest-asyncio" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and extra != 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2'", + "python_full_version >= '3.10' and python_full_version < '3.14' and extra != 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2'", + "python_full_version >= '3.10' and extra == 'group-9-stagehand-pydantic-v1' and extra != 'group-9-stagehand-pydantic-v2'", + "python_full_version >= '3.10' and extra != 'group-9-stagehand-pydantic-v1' and extra != 'group-9-stagehand-pydantic-v2'", +] +dependencies = [ + { name = "backports-asyncio-runner", marker = "python_full_version == '3.10.*' or (extra == 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2')" }, + { name = "pytest", version = "9.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10' or (extra == 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2')" }, + { name = "typing-extensions", marker = "(python_full_version >= '3.10' and python_full_version < '3.13') or (python_full_version < '3.10' and extra == 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2') or (python_full_version >= '3.13' and extra == 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/90/2c/8af215c0f776415f3590cac4f9086ccefd6fd463befeae41cd4d3f193e5a/pytest_asyncio-1.3.0.tar.gz", hash = "sha256:d7f52f36d231b80ee124cd216ffb19369aa168fc10095013c6b014a34d3ee9e5", size = 50087, upload-time = "2025-11-10T16:07:47.256Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/35/f8b19922b6a25bc0880171a2f1a003eaeb93657475193ab516fd87cac9da/pytest_asyncio-1.3.0-py3-none-any.whl", hash = "sha256:611e26147c7f77640e6d0a92a38ed17c3e9848063698d5c93d5aa7aa11cebff5", size = 15075, upload-time = "2025-11-10T16:07:45.537Z" }, +] + +[[package]] +name = "pytest-xdist" +version = "3.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "execnet" }, + { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10' or (extra == 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2')" }, + { name = "pytest", version = "9.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10' or (extra == 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/78/b4/439b179d1ff526791eb921115fca8e44e596a13efeda518b9d845a619450/pytest_xdist-3.8.0.tar.gz", hash = "sha256:7e578125ec9bc6050861aa93f2d59f1d8d085595d6551c2c90b6f4fad8d3a9f1", size = 88069, upload-time = "2025-07-01T13:30:59.346Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl", hash = "sha256:202ca578cfeb7370784a8c33d6d05bc6e13b4f25b5053c30a152269fd10f0b88", size = 46396, upload-time = "2025-07-01T13:30:56.632Z" }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six", marker = "python_full_version < '3.10' or (extra == 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, +] + +[[package]] +name = "respx" +version = "0.22.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "httpx" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f4/7c/96bd0bc759cf009675ad1ee1f96535edcb11e9666b985717eb8c87192a95/respx-0.22.0.tar.gz", hash = "sha256:3c8924caa2a50bd71aefc07aa812f2466ff489f1848c96e954a5362d17095d91", size = 28439, upload-time = "2024-12-19T22:33:59.374Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8e/67/afbb0978d5399bc9ea200f1d4489a23c9a1dad4eee6376242b8182389c79/respx-0.22.0-py2.py3-none-any.whl", hash = "sha256:631128d4c9aba15e56903fb5f66fb1eff412ce28dd387ca3a81339e52dbd3ad0", size = 25127, upload-time = "2024-12-19T22:33:57.837Z" }, +] + +[[package]] +name = "rich" +version = "14.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py", version = "3.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10' or (extra == 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2')" }, + { name = "markdown-it-py", version = "4.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10' or (extra == 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2')" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fb/d2/8920e102050a0de7bfabeb4c4614a49248cf8d5d7a8d01885fbb24dc767a/rich-14.2.0.tar.gz", hash = "sha256:73ff50c7c0c1c77c8243079283f4edb376f0f6442433aecb8ce7e6d0b92d1fe4", size = 219990, upload-time = "2025-10-09T14:16:53.064Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/25/7a/b0178788f8dc6cafce37a212c99565fa1fe7872c70c6c9c1e1a372d9d88f/rich-14.2.0-py3-none-any.whl", hash = "sha256:76bc51fe2e57d2b1be1f96c524b890b816e334ab4c1e45888799bfaab0021edd", size = 243393, upload-time = "2025-10-09T14:16:51.245Z" }, +] + +[[package]] +name = "ruff" +version = "0.14.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b7/5b/dd7406afa6c95e3d8fa9d652b6d6dd17dd4a6bf63cb477014e8ccd3dcd46/ruff-0.14.7.tar.gz", hash = "sha256:3417deb75d23bd14a722b57b0a1435561db65f0ad97435b4cf9f85ffcef34ae5", size = 5727324, upload-time = "2025-11-28T20:55:10.525Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8c/b1/7ea5647aaf90106f6d102230e5df874613da43d1089864da1553b899ba5e/ruff-0.14.7-py3-none-linux_armv6l.whl", hash = "sha256:b9d5cb5a176c7236892ad7224bc1e63902e4842c460a0b5210701b13e3de4fca", size = 13414475, upload-time = "2025-11-28T20:54:54.569Z" }, + { url = "https://files.pythonhosted.org/packages/af/19/fddb4cd532299db9cdaf0efdc20f5c573ce9952a11cb532d3b859d6d9871/ruff-0.14.7-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:3f64fe375aefaf36ca7d7250292141e39b4cea8250427482ae779a2aa5d90015", size = 13634613, upload-time = "2025-11-28T20:55:17.54Z" }, + { url = "https://files.pythonhosted.org/packages/40/2b/469a66e821d4f3de0440676ed3e04b8e2a1dc7575cf6fa3ba6d55e3c8557/ruff-0.14.7-py3-none-macosx_11_0_arm64.whl", hash = "sha256:93e83bd3a9e1a3bda64cb771c0d47cda0e0d148165013ae2d3554d718632d554", size = 12765458, upload-time = "2025-11-28T20:55:26.128Z" }, + { url = "https://files.pythonhosted.org/packages/f1/05/0b001f734fe550bcfde4ce845948ac620ff908ab7241a39a1b39bb3c5f49/ruff-0.14.7-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3838948e3facc59a6070795de2ae16e5786861850f78d5914a03f12659e88f94", size = 13236412, upload-time = "2025-11-28T20:55:28.602Z" }, + { url = "https://files.pythonhosted.org/packages/11/36/8ed15d243f011b4e5da75cd56d6131c6766f55334d14ba31cce5461f28aa/ruff-0.14.7-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:24c8487194d38b6d71cd0fd17a5b6715cda29f59baca1defe1e3a03240f851d1", size = 13182949, upload-time = "2025-11-28T20:55:33.265Z" }, + { url = "https://files.pythonhosted.org/packages/3b/cf/fcb0b5a195455729834f2a6eadfe2e4519d8ca08c74f6d2b564a4f18f553/ruff-0.14.7-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:79c73db6833f058a4be8ffe4a0913b6d4ad41f6324745179bd2aa09275b01d0b", size = 13816470, upload-time = "2025-11-28T20:55:08.203Z" }, + { url = "https://files.pythonhosted.org/packages/7f/5d/34a4748577ff7a5ed2f2471456740f02e86d1568a18c9faccfc73bd9ca3f/ruff-0.14.7-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:12eb7014fccff10fc62d15c79d8a6be4d0c2d60fe3f8e4d169a0d2def75f5dad", size = 15289621, upload-time = "2025-11-28T20:55:30.837Z" }, + { url = "https://files.pythonhosted.org/packages/53/53/0a9385f047a858ba133d96f3f8e3c9c66a31cc7c4b445368ef88ebeac209/ruff-0.14.7-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6c623bbdc902de7ff715a93fa3bb377a4e42dd696937bf95669118773dbf0c50", size = 14975817, upload-time = "2025-11-28T20:55:24.107Z" }, + { url = "https://files.pythonhosted.org/packages/a8/d7/2f1c32af54c3b46e7fadbf8006d8b9bcfbea535c316b0bd8813d6fb25e5d/ruff-0.14.7-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f53accc02ed2d200fa621593cdb3c1ae06aa9b2c3cae70bc96f72f0000ae97a9", size = 14284549, upload-time = "2025-11-28T20:55:06.08Z" }, + { url = "https://files.pythonhosted.org/packages/92/05/434ddd86becd64629c25fb6b4ce7637dd52a45cc4a4415a3008fe61c27b9/ruff-0.14.7-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:281f0e61a23fcdcffca210591f0f53aafaa15f9025b5b3f9706879aaa8683bc4", size = 14071389, upload-time = "2025-11-28T20:55:35.617Z" }, + { url = "https://files.pythonhosted.org/packages/ff/50/fdf89d4d80f7f9d4f420d26089a79b3bb1538fe44586b148451bc2ba8d9c/ruff-0.14.7-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:dbbaa5e14148965b91cb090236931182ee522a5fac9bc5575bafc5c07b9f9682", size = 14202679, upload-time = "2025-11-28T20:55:01.472Z" }, + { url = "https://files.pythonhosted.org/packages/77/54/87b34988984555425ce967f08a36df0ebd339bb5d9d0e92a47e41151eafc/ruff-0.14.7-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:1464b6e54880c0fe2f2d6eaefb6db15373331414eddf89d6b903767ae2458143", size = 13147677, upload-time = "2025-11-28T20:55:19.933Z" }, + { url = "https://files.pythonhosted.org/packages/67/29/f55e4d44edfe053918a16a3299e758e1c18eef216b7a7092550d7a9ec51c/ruff-0.14.7-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:f217ed871e4621ea6128460df57b19ce0580606c23aeab50f5de425d05226784", size = 13151392, upload-time = "2025-11-28T20:55:21.967Z" }, + { url = "https://files.pythonhosted.org/packages/36/69/47aae6dbd4f1d9b4f7085f4d9dcc84e04561ee7ad067bf52e0f9b02e3209/ruff-0.14.7-py3-none-musllinux_1_2_i686.whl", hash = "sha256:6be02e849440ed3602d2eb478ff7ff07d53e3758f7948a2a598829660988619e", size = 13412230, upload-time = "2025-11-28T20:55:12.749Z" }, + { url = "https://files.pythonhosted.org/packages/b7/4b/6e96cb6ba297f2ba502a231cd732ed7c3de98b1a896671b932a5eefa3804/ruff-0.14.7-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:19a0f116ee5e2b468dfe80c41c84e2bbd6b74f7b719bee86c2ecde0a34563bcc", size = 14195397, upload-time = "2025-11-28T20:54:56.896Z" }, + { url = "https://files.pythonhosted.org/packages/69/82/251d5f1aa4dcad30aed491b4657cecd9fb4274214da6960ffec144c260f7/ruff-0.14.7-py3-none-win32.whl", hash = "sha256:e33052c9199b347c8937937163b9b149ef6ab2e4bb37b042e593da2e6f6cccfa", size = 13126751, upload-time = "2025-11-28T20:55:03.47Z" }, + { url = "https://files.pythonhosted.org/packages/a8/b5/d0b7d145963136b564806f6584647af45ab98946660d399ec4da79cae036/ruff-0.14.7-py3-none-win_amd64.whl", hash = "sha256:e17a20ad0d3fad47a326d773a042b924d3ac31c6ca6deb6c72e9e6b5f661a7c6", size = 14531726, upload-time = "2025-11-28T20:54:59.121Z" }, + { url = "https://files.pythonhosted.org/packages/1d/d2/1637f4360ada6a368d3265bf39f2cf737a0aaab15ab520fc005903e883f8/ruff-0.14.7-py3-none-win_arm64.whl", hash = "sha256:be4d653d3bea1b19742fcc6502354e32f65cd61ff2fbdb365803ef2c2aec6228", size = 13609215, upload-time = "2025-11-28T20:55:15.375Z" }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, +] + +[[package]] +name = "stagehand" +version = "0.0.1" +source = { editable = "." } +dependencies = [ + { name = "anyio" }, + { name = "distro" }, + { name = "httpx" }, + { name = "pydantic", version = "1.10.24", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'group-9-stagehand-pydantic-v1'" }, + { name = "pydantic", version = "2.12.5", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'group-9-stagehand-pydantic-v2' or extra != 'group-9-stagehand-pydantic-v1'" }, + { name = "sniffio" }, + { name = "typing-extensions" }, +] + +[package.optional-dependencies] +aiohttp = [ + { name = "aiohttp" }, + { name = "httpx-aiohttp" }, +] + +[package.dev-dependencies] +dev = [ + { name = "dirty-equals" }, + { name = "importlib-metadata" }, + { name = "mypy" }, + { name = "pyright" }, + { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10' or (extra == 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2')" }, + { name = "pytest", version = "9.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10' or (extra == 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2')" }, + { name = "pytest-asyncio", version = "1.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10' or (extra == 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2')" }, + { name = "pytest-asyncio", version = "1.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10' or (extra == 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2')" }, + { name = "pytest-xdist" }, + { name = "respx" }, + { name = "rich" }, + { name = "ruff" }, + { name = "time-machine", version = "2.19.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10' or (extra == 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2')" }, + { name = "time-machine", version = "3.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10' or (extra == 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2')" }, +] +pydantic-v1 = [ + { name = "pydantic", version = "1.10.24", source = { registry = "https://pypi.org/simple" } }, +] +pydantic-v2 = [ + { name = "pydantic", version = "2.12.5", source = { registry = "https://pypi.org/simple" } }, +] + +[package.metadata] +requires-dist = [ + { name = "aiohttp", marker = "extra == 'aiohttp'" }, + { name = "anyio", specifier = ">=3.5.0,<5" }, + { name = "distro", specifier = ">=1.7.0,<2" }, + { name = "httpx", specifier = ">=0.23.0,<1" }, + { name = "httpx-aiohttp", marker = "extra == 'aiohttp'", specifier = ">=0.1.9" }, + { name = "pydantic", specifier = ">=1.9.0,<3" }, + { name = "sniffio" }, + { name = "typing-extensions", specifier = ">=4.10,<5" }, +] +provides-extras = ["aiohttp"] + +[package.metadata.requires-dev] +dev = [ + { name = "dirty-equals", specifier = ">=0.6.0" }, + { name = "importlib-metadata", specifier = ">=6.7.0" }, + { name = "mypy", specifier = "==1.17" }, + { name = "pyright", specifier = "==1.1.399" }, + { name = "pytest" }, + { name = "pytest-asyncio" }, + { name = "pytest-xdist", specifier = ">=3.6.1" }, + { name = "respx" }, + { name = "rich", specifier = ">=13.7.1" }, + { name = "ruff" }, + { name = "time-machine" }, +] +pydantic-v1 = [{ name = "pydantic", specifier = ">=1.9.0,<2" }] +pydantic-v2 = [ + { name = "pydantic", marker = "python_full_version < '3.14'", specifier = "~=2.0" }, + { name = "pydantic", marker = "python_full_version >= '3.14'", specifier = "~=2.12" }, +] + +[[package]] +name = "time-machine" +version = "2.19.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "python-dateutil", marker = "python_full_version < '3.10' or (extra == 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f8/a4/1b5fdd165f61b67f445fac2a7feb0c655118edef429cd09ff5a8067f7f1d/time_machine-2.19.0.tar.gz", hash = "sha256:7c5065a8b3f2bbb449422c66ef71d114d3f909c276a6469642ecfffb6a0fcd29", size = 14576, upload-time = "2025-08-19T17:22:08.402Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9d/8f/19125611ebbcb3a14da14cd982b9eb4573e2733db60c9f1fbf6a39534f40/time_machine-2.19.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b5169018ef47206997b46086ce01881cd3a4666fd2998c9d76a87858ca3e49e9", size = 19659, upload-time = "2025-08-19T17:20:30.062Z" }, + { url = "https://files.pythonhosted.org/packages/74/da/9b0a928321e7822a3ff96dbd1eae089883848e30e9e1b149b85fb96ba56b/time_machine-2.19.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:85bb7ed440fccf6f6d0c8f7d68d849e7c3d1f771d5e0b2cdf871fa6561da569f", size = 15157, upload-time = "2025-08-19T17:20:31.931Z" }, + { url = "https://files.pythonhosted.org/packages/36/ff/d7e943422038f5f2161fe2c2d791e64a45be691ef946020b20f3a6efc4d4/time_machine-2.19.0-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a3b12028af1cdc09ccd595be2168b7b26f206c1e190090b048598fbe278beb8e", size = 32860, upload-time = "2025-08-19T17:20:33.241Z" }, + { url = "https://files.pythonhosted.org/packages/fc/80/2b0f1070ed9808ee7da7a6da62a4a0b776957cb4d861578348f86446e778/time_machine-2.19.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c261f073086cf081d1443cbf7684148c662659d3d139d06b772bfe3fe7cc71a6", size = 34510, upload-time = "2025-08-19T17:20:34.221Z" }, + { url = "https://files.pythonhosted.org/packages/ef/b4/48038691c8d89924b36c83335a73adeeb68c884f5a1da08a5b17b8a956f3/time_machine-2.19.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:011954d951230a9f1079f22b39ed1a3a9abb50ee297dfb8c557c46351659d94d", size = 36204, upload-time = "2025-08-19T17:20:35.163Z" }, + { url = "https://files.pythonhosted.org/packages/37/2e/60e8adb541df195e83cb74b720b2cfb1f22ed99c5a7f8abf2a9ab3442cb5/time_machine-2.19.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b0f83308b29c7872006803f2e77318874eb84d0654f2afe0e48e3822e7a2e39b", size = 34936, upload-time = "2025-08-19T17:20:36.61Z" }, + { url = "https://files.pythonhosted.org/packages/5e/72/e8cee59c6cd99dd3b25b8001a0253e779a286aa8f44d5b40777cbd66210b/time_machine-2.19.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:39733ef844e2984620ec9382a42d00cccc4757d75a5dd572be8c2572e86e50b9", size = 32932, upload-time = "2025-08-19T17:20:37.901Z" }, + { url = "https://files.pythonhosted.org/packages/2c/eb/83f300d93c1504965d944e03679f1c943a923bce2d0fdfadef0e2e22cc13/time_machine-2.19.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f8db99f6334432e9ffbf00c215caf2ae9773f17cec08304d77e9e90febc3507b", size = 34010, upload-time = "2025-08-19T17:20:39.202Z" }, + { url = "https://files.pythonhosted.org/packages/e1/77/f35f2500e04daac5033a22fbfd17e68467822b8406ee77966bf222ccaa26/time_machine-2.19.0-cp310-cp310-win32.whl", hash = "sha256:72bf66cd19e27ffd26516b9cbe676d50c2e0b026153289765dfe0cf406708128", size = 17121, upload-time = "2025-08-19T17:20:40.108Z" }, + { url = "https://files.pythonhosted.org/packages/db/df/32d3e0404be1760a64a44caab2af34b07e952bfe00a23134fea9ddba3e8a/time_machine-2.19.0-cp310-cp310-win_amd64.whl", hash = "sha256:46f1c945934ce3d6b4f388b8e581fce7f87ec891ea90d7128e19520e434f96f0", size = 17957, upload-time = "2025-08-19T17:20:41.079Z" }, + { url = "https://files.pythonhosted.org/packages/66/df/598a71a1afb4b509a4587273b76590b16d9110a3e9106f01eedc68d02bb2/time_machine-2.19.0-cp310-cp310-win_arm64.whl", hash = "sha256:fb4897c7a5120a4fd03f0670f332d83b7e55645886cd8864a71944c4c2e5b35b", size = 16821, upload-time = "2025-08-19T17:20:41.967Z" }, + { url = "https://files.pythonhosted.org/packages/1d/ed/4815ebcc9b6c14273f692b9be38a9b09eae52a7e532407cc61a51912b121/time_machine-2.19.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5ee91664880434d98e41585c3446dac7180ec408c786347451ddfca110d19296", size = 19342, upload-time = "2025-08-19T17:20:43.207Z" }, + { url = "https://files.pythonhosted.org/packages/ee/08/154cce8b11b60d8238b0b751b8901d369999f4e8f7c3a5f917caa5d95b0b/time_machine-2.19.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ed3732b83a893d1c7b8cabde762968b4dc5680ee0d305b3ecca9bb516f4e3862", size = 14978, upload-time = "2025-08-19T17:20:44.134Z" }, + { url = "https://files.pythonhosted.org/packages/c7/b7/b689d8c8eeca7af375cfcd64973e49e83aa817cc00f80f98548d42c0eb50/time_machine-2.19.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6ba0303e9cc9f7f947e344f501e26bedfb68fab521e3c2729d370f4f332d2d55", size = 30964, upload-time = "2025-08-19T17:20:45.366Z" }, + { url = "https://files.pythonhosted.org/packages/80/91/38bf9c79674e95ce32e23c267055f281dff651eec77ed32a677db3dc011a/time_machine-2.19.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2851825b524a988ee459c37c1c26bdfaa7eff78194efb2b562ea497a6f375b0a", size = 32606, upload-time = "2025-08-19T17:20:46.693Z" }, + { url = "https://files.pythonhosted.org/packages/19/4a/e9222d85d4de68975a5e799f539a9d32f3a134a9101fca0a61fa6aa33d68/time_machine-2.19.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:68d32b09ecfd7fef59255c091e8e7c24dd117f882c4880b5c7ab8c5c32a98f89", size = 34405, upload-time = "2025-08-19T17:20:48.032Z" }, + { url = "https://files.pythonhosted.org/packages/14/e2/09480d608d42d6876f9ff74593cfc9197a7eb2c31381a74fb2b145575b65/time_machine-2.19.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:60c46ab527bf2fa144b530f639cc9e12803524c9e1f111dc8c8f493bb6586eeb", size = 33181, upload-time = "2025-08-19T17:20:48.937Z" }, + { url = "https://files.pythonhosted.org/packages/84/64/f9359e000fad32d9066305c48abc527241d608bcdf77c19d67d66e268455/time_machine-2.19.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:56f26ab9f0201c453d18fe76bb7d1cf05fe58c1b9d9cb0c7d243d05132e01292", size = 31036, upload-time = "2025-08-19T17:20:50.276Z" }, + { url = "https://files.pythonhosted.org/packages/71/0d/fab2aacec71e3e482bd7fce0589381f9414a4a97f8766bddad04ad047b7b/time_machine-2.19.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6c806cf3c1185baa1d807b7f51bed0db7a6506832c961d5d1b4c94c775749bc0", size = 32145, upload-time = "2025-08-19T17:20:51.449Z" }, + { url = "https://files.pythonhosted.org/packages/44/fb/faeba2405fb27553f7b28db441a500e2064ffdb2dcba001ee315fdd2c121/time_machine-2.19.0-cp311-cp311-win32.whl", hash = "sha256:b30039dfd89855c12138095bee39c540b4633cbc3684580d684ef67a99a91587", size = 17004, upload-time = "2025-08-19T17:20:52.38Z" }, + { url = "https://files.pythonhosted.org/packages/2f/84/87e483d660ca669426192969280366635c845c3154a9fe750be546ed3afc/time_machine-2.19.0-cp311-cp311-win_amd64.whl", hash = "sha256:13ed8b34430f1de79905877f5600adffa626793ab4546a70a99fb72c6a3350d8", size = 17822, upload-time = "2025-08-19T17:20:53.348Z" }, + { url = "https://files.pythonhosted.org/packages/41/f4/ebf7bbf5047854a528adaf54a5e8780bc5f7f0104c298ab44566a3053bf8/time_machine-2.19.0-cp311-cp311-win_arm64.whl", hash = "sha256:cc29a50a0257d8750b08056b66d7225daab47606832dea1a69e8b017323bf511", size = 16680, upload-time = "2025-08-19T17:20:54.26Z" }, + { url = "https://files.pythonhosted.org/packages/9b/aa/7e00614d339e4d687f6e96e312a1566022528427d237ec639df66c4547bc/time_machine-2.19.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:c85cf437dc3c07429456d8d6670ac90ecbd8241dcd0fbf03e8db2800576f91ff", size = 19308, upload-time = "2025-08-19T17:20:55.25Z" }, + { url = "https://files.pythonhosted.org/packages/ab/3c/bde3c757394f5bca2fbc1528d4117960a26c38f9b160bf471b38d2378d8f/time_machine-2.19.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d9238897e8ef54acdf59f5dff16f59ca0720e7c02d820c56b4397c11db5d3eb9", size = 15019, upload-time = "2025-08-19T17:20:56.204Z" }, + { url = "https://files.pythonhosted.org/packages/c8/e0/8ca916dd918018352d377f1f5226ee071cfbeb7dbbde2b03d14a411ac2b1/time_machine-2.19.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e312c7d5d6bfffb96c6a7b39ff29e3046de100d7efaa3c01552654cfbd08f14c", size = 33079, upload-time = "2025-08-19T17:20:57.166Z" }, + { url = "https://files.pythonhosted.org/packages/48/69/184a0209f02dd0cb5e01e8d13cd4c97a5f389c4e3d09b95160dd676ad1e7/time_machine-2.19.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:714c40b2c90d1c57cc403382d5a9cf16e504cb525bfe9650095317da3c3d62b5", size = 34925, upload-time = "2025-08-19T17:20:58.117Z" }, + { url = "https://files.pythonhosted.org/packages/43/42/4bbf4309e8e57cea1086eb99052d97ff6ddecc1ab6a3b07aa4512f8bf963/time_machine-2.19.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2eaa1c675d500dc3ccae19e9fb1feff84458a68c132bbea47a80cc3dd2df7072", size = 36384, upload-time = "2025-08-19T17:20:59.108Z" }, + { url = "https://files.pythonhosted.org/packages/b1/af/9f510dc1719157348c1a2e87423aed406589070b54b503cb237d9bf3a4fe/time_machine-2.19.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e77a414e9597988af53b2b2e67242c9d2f409769df0d264b6d06fda8ca3360d4", size = 34881, upload-time = "2025-08-19T17:21:00.116Z" }, + { url = "https://files.pythonhosted.org/packages/ca/28/61764a635c70cc76c76ba582dfdc1a84834cddaeb96789023af5214426b2/time_machine-2.19.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:cd93996970e11c382b04d4937c3cd0b0167adeef14725ece35aae88d8a01733c", size = 32931, upload-time = "2025-08-19T17:21:01.095Z" }, + { url = "https://files.pythonhosted.org/packages/b6/e0/f028d93b266e6ade8aca5851f76ebbc605b2905cdc29981a2943b43e1a6c/time_machine-2.19.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8e20a6d8d6e23174bd7e931e134d9610b136db460b249d07e84ecdad029ec352", size = 34241, upload-time = "2025-08-19T17:21:02.052Z" }, + { url = "https://files.pythonhosted.org/packages/7d/a6/36d1950ed1d3f613158024cf1dcc73db1d9ef0b9117cf51ef2e37dc06499/time_machine-2.19.0-cp312-cp312-win32.whl", hash = "sha256:95afc9bc65228b27be80c2756799c20b8eb97c4ef382a9b762b6d7888bc84099", size = 17021, upload-time = "2025-08-19T17:21:03.374Z" }, + { url = "https://files.pythonhosted.org/packages/b1/0d/e2dce93355abda3cac69e77fe96566757e98b8fe7fdcbddce89c9ced3f5f/time_machine-2.19.0-cp312-cp312-win_amd64.whl", hash = "sha256:e84909af950e2448f4e2562ea5759c946248c99ab380d2b47d79b62bd76fa236", size = 17857, upload-time = "2025-08-19T17:21:04.331Z" }, + { url = "https://files.pythonhosted.org/packages/eb/28/50ae6fb83b7feeeca7a461c0dc156cf7ef5e6ef594a600d06634fde6a2cb/time_machine-2.19.0-cp312-cp312-win_arm64.whl", hash = "sha256:0390a1ea9fa7e9d772a39b7c61b34fdcca80eb9ffac339cc0441c6c714c81470", size = 16677, upload-time = "2025-08-19T17:21:05.39Z" }, + { url = "https://files.pythonhosted.org/packages/a9/b8/24ebce67aa531bae2cbe164bb3f4abc6467dc31f3aead35e77f5a075ea3e/time_machine-2.19.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:5e172866753e6041d3b29f3037dc47c20525176a494a71bbd0998dfdc4f11f2f", size = 19373, upload-time = "2025-08-19T17:21:06.701Z" }, + { url = "https://files.pythonhosted.org/packages/53/a5/c9a5240fd2f845d3ff9fa26f8c8eaa29f7239af9d65007e61d212250f15b/time_machine-2.19.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f70f68379bd6f542ae6775cce9a4fa3dcc20bf7959c42eaef871c14469e18097", size = 15056, upload-time = "2025-08-19T17:21:07.667Z" }, + { url = "https://files.pythonhosted.org/packages/b9/92/66cce5d2fb2a5e68459aca85fd18a7e2d216f725988940cd83f96630f2f1/time_machine-2.19.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e69e0b0f694728a00e72891ef8dd00c7542952cb1c87237db594b6b27d504a96", size = 33172, upload-time = "2025-08-19T17:21:08.619Z" }, + { url = "https://files.pythonhosted.org/packages/ae/20/b499e9ab4364cd466016c33dcdf4f56629ca4c20b865bd4196d229f31d92/time_machine-2.19.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3ae0a8b869574301ec5637e32c270c7384cca5cd6e230f07af9d29271a7fa293", size = 35042, upload-time = "2025-08-19T17:21:09.622Z" }, + { url = "https://files.pythonhosted.org/packages/41/32/b252d3d32791eb16c07d553c820dbc33d9c7fa771de3d1c602190bded2b7/time_machine-2.19.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:554e4317de90e2f7605ff80d153c8bb56b38c0d0c0279feb17e799521e987b8c", size = 36535, upload-time = "2025-08-19T17:21:10.571Z" }, + { url = "https://files.pythonhosted.org/packages/98/cf/4d0470062b9742e1b040ab81bad04d1a5d1de09806507bb6188989cfa1a7/time_machine-2.19.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6567a5ec5538ed550539ac29be11b3cb36af1f9894e2a72940cba0292cc7c3c9", size = 34945, upload-time = "2025-08-19T17:21:11.538Z" }, + { url = "https://files.pythonhosted.org/packages/24/71/2f741b29d98b1c18f6777a32236497c3d3264b6077e431cea4695684c8a1/time_machine-2.19.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:82e9ffe8dfff07b0d810a2ad015a82cd78c6a237f6c7cf185fa7f747a3256f8a", size = 33014, upload-time = "2025-08-19T17:21:12.858Z" }, + { url = "https://files.pythonhosted.org/packages/e8/83/ca8dba6106562843fd99f672e5aaf95badbc10f4f13f7cfe8d8640a7019d/time_machine-2.19.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7e1c4e578cdd69b3531d8dd3fbcb92a0cd879dadb912ee37af99c3a9e3c0d285", size = 34350, upload-time = "2025-08-19T17:21:13.923Z" }, + { url = "https://files.pythonhosted.org/packages/21/7f/34fe540450e18d0a993240100e4b86e8d03d831b92af8bb6ddb2662dc6fc/time_machine-2.19.0-cp313-cp313-win32.whl", hash = "sha256:72dbd4cbc3d96dec9dd281ddfbb513982102776b63e4e039f83afb244802a9e5", size = 17047, upload-time = "2025-08-19T17:21:14.874Z" }, + { url = "https://files.pythonhosted.org/packages/bf/5d/c8be73df82c7ebe7cd133279670e89b8b110af3ce1412c551caa9d08e625/time_machine-2.19.0-cp313-cp313-win_amd64.whl", hash = "sha256:e17e3e089ac95f9a145ce07ff615e3c85674f7de36f2d92aaf588493a23ffb4b", size = 17868, upload-time = "2025-08-19T17:21:15.819Z" }, + { url = "https://files.pythonhosted.org/packages/92/13/2dfd3b8fb285308f61cd7aa9bfa96f46ddf916e3549a0f0afd094c556599/time_machine-2.19.0-cp313-cp313-win_arm64.whl", hash = "sha256:149072aff8e3690e14f4916103d898ea0d5d9c95531b6aa0995251c299533f7b", size = 16710, upload-time = "2025-08-19T17:21:16.748Z" }, + { url = "https://files.pythonhosted.org/packages/05/c1/deebb361727d2c5790f9d4d874be1b19afd41f4375581df465e6718b46a2/time_machine-2.19.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:f3589fee1ed0ab6ee424a55b0ea1ec694c4ba64cc26895bcd7d99f3d1bc6a28a", size = 20053, upload-time = "2025-08-19T17:21:17.704Z" }, + { url = "https://files.pythonhosted.org/packages/45/e8/fe3376951e6118d8ec1d1f94066a169b791424fe4a26c7dfc069b153ee08/time_machine-2.19.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:7887e85275c4975fe54df03dcdd5f38bd36be973adc68a8c77e17441c3b443d6", size = 15423, upload-time = "2025-08-19T17:21:18.668Z" }, + { url = "https://files.pythonhosted.org/packages/9c/c7/f88d95cd1a87c650cf3749b4d64afdaf580297aa18ad7f4b44ec9d252dfc/time_machine-2.19.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:ce0be294c209928563fcce1c587963e60ec803436cf1e181acd5bc1e425d554b", size = 39630, upload-time = "2025-08-19T17:21:19.645Z" }, + { url = "https://files.pythonhosted.org/packages/cc/5d/65a5c48a65357e56ec6f032972e4abd1c02d4fca4b0717a3aaefd19014d4/time_machine-2.19.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a62fd1ab380012c86f4c042010418ed45eb31604f4bf4453e17c9fa60bc56a29", size = 41242, upload-time = "2025-08-19T17:21:20.979Z" }, + { url = "https://files.pythonhosted.org/packages/f6/f9/fe5209e1615fde0a8cad6c4e857157b150333ed1fe31a7632b08cfe0ebdd/time_machine-2.19.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b25ec853a4530a5800731257f93206b12cbdee85ede964ebf8011b66086a7914", size = 44278, upload-time = "2025-08-19T17:21:21.984Z" }, + { url = "https://files.pythonhosted.org/packages/4a/3a/a5e5fe9c5d614cde0a9387ff35e8dfd12c5ef6384e4c1a21b04e6e0b905d/time_machine-2.19.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a430e4d0e0556f021a9c78e9b9f68e5e8910bdace4aa34ed4d1a73e239ed9384", size = 42321, upload-time = "2025-08-19T17:21:23.755Z" }, + { url = "https://files.pythonhosted.org/packages/a1/c5/56eca774e9162bc1ce59111d2bd69140dc8908c9478c92ec7bd15d547600/time_machine-2.19.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:2415b7495ec4364c8067071e964fbadfe746dd4cdb43983f2f0bd6ebed13315c", size = 39270, upload-time = "2025-08-19T17:21:26.009Z" }, + { url = "https://files.pythonhosted.org/packages/9b/69/5dd0c420667578169a12acc8c8fd7452e8cfb181e41c9b4ac7e88fa36686/time_machine-2.19.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dbfc6b90c10f288594e1bf89a728a98cc0030791fd73541bbdc6b090aff83143", size = 40193, upload-time = "2025-08-19T17:21:27.054Z" }, + { url = "https://files.pythonhosted.org/packages/75/a7/de974d421bd55c9355583427c2a38fb0237bb5fd6614af492ba89dacb2f9/time_machine-2.19.0-cp313-cp313t-win32.whl", hash = "sha256:16f5d81f650c0a4d117ab08036dc30b5f8b262e11a4a0becc458e7f1c011b228", size = 17542, upload-time = "2025-08-19T17:21:28.674Z" }, + { url = "https://files.pythonhosted.org/packages/76/0a/aa0d05becd5d06ae8d3f16d657dc8cc9400c8d79aef80299de196467ff12/time_machine-2.19.0-cp313-cp313t-win_amd64.whl", hash = "sha256:645699616ec14e147094f601e6ab9553ff6cea37fad9c42720a6d7ed04bcd5dc", size = 18703, upload-time = "2025-08-19T17:21:29.663Z" }, + { url = "https://files.pythonhosted.org/packages/1f/c0/f785a4c7c73aa176510f7c48b84b49c26be84af0d534deb222e0327f750e/time_machine-2.19.0-cp313-cp313t-win_arm64.whl", hash = "sha256:b32daa965d13237536ea3afaa5ad61ade2b2d9314bc3a20196a0d2e1d7b57c6a", size = 17020, upload-time = "2025-08-19T17:21:30.653Z" }, + { url = "https://files.pythonhosted.org/packages/ed/97/c5fb51def06c0b2b6735332ad118ab35b4d9b85368792e5b638e99b1b686/time_machine-2.19.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:31cb43c8fd2d961f31bed0ff4e0026964d2b35e5de9e0fabbfecf756906d3612", size = 19360, upload-time = "2025-08-19T17:21:31.94Z" }, + { url = "https://files.pythonhosted.org/packages/2d/4e/2d795f7d6b7f5205ffe737a05bb1cf19d8038233b797062b2ef412b8512b/time_machine-2.19.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:bdf481a75afc6bff3e520db594501975b652f7def21cd1de6aa971d35ba644e6", size = 15033, upload-time = "2025-08-19T17:21:32.934Z" }, + { url = "https://files.pythonhosted.org/packages/dd/32/9bad501e360b4e758c58fae616ca5f8c7ad974b343f2463a15b2bf77a366/time_machine-2.19.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:00bee4bb950ac6a08d62af78e4da0cf2b4fc2abf0de2320d0431bf610db06e7c", size = 33379, upload-time = "2025-08-19T17:21:33.925Z" }, + { url = "https://files.pythonhosted.org/packages/a3/45/eda0ca4d793dfd162478d6163759b1c6ce7f6e61daa7fd7d62b31f21f87f/time_machine-2.19.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9f02199490906582302ce09edd32394fb393271674c75d7aa76c7a3245f16003", size = 35123, upload-time = "2025-08-19T17:21:34.945Z" }, + { url = "https://files.pythonhosted.org/packages/f0/5a/97e16325442ae5731fcaac794f0a1ef9980eff8a5491e58201d7eb814a34/time_machine-2.19.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e35726c7ba625f844c13b1fc0d4f81f394eefaee1d3a094a9093251521f2ef15", size = 36588, upload-time = "2025-08-19T17:21:35.975Z" }, + { url = "https://files.pythonhosted.org/packages/e8/9d/bf0b2ccc930cc4a316f26f1c78d3f313cd0fa13bb7480369b730a8f129db/time_machine-2.19.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:304315023999cd401ff02698870932b893369e1cfeb2248d09f6490507a92e97", size = 35013, upload-time = "2025-08-19T17:21:37.017Z" }, + { url = "https://files.pythonhosted.org/packages/f0/5a/39ac6a3078174f9715d88364871348b249631f12e76de1b862433b3f8862/time_machine-2.19.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:9765d4f003f263ea8bfd90d2d15447ca4b3dfa181922cf6cf808923b02ac180a", size = 33303, upload-time = "2025-08-19T17:21:38.352Z" }, + { url = "https://files.pythonhosted.org/packages/b3/ac/d8646baf9f95f2e792a6d7a7b35e92fca253c4a992afff801beafae0e5c2/time_machine-2.19.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7837ef3fd5911eb9b480909bb93d922737b6bdecea99dfcedb0a03807de9b2d3", size = 34440, upload-time = "2025-08-19T17:21:39.382Z" }, + { url = "https://files.pythonhosted.org/packages/ce/8b/8b6568c5ae966d80ead03ab537be3c6acf2af06fb501c2d466a3162c6295/time_machine-2.19.0-cp314-cp314-win32.whl", hash = "sha256:4bb5bd43b1bdfac3007b920b51d8e761f024ed465cfeec63ac4296922a4ec428", size = 17162, upload-time = "2025-08-19T17:21:40.381Z" }, + { url = "https://files.pythonhosted.org/packages/46/a5/211c1ab4566eba5308b2dc001b6349e3a032e3f6afa67ca2f27ea6b27af5/time_machine-2.19.0-cp314-cp314-win_amd64.whl", hash = "sha256:f583bbd0aa8ab4a7c45a684bf636d9e042d466e30bcbae1d13e7541e2cbe7207", size = 18040, upload-time = "2025-08-19T17:21:41.363Z" }, + { url = "https://files.pythonhosted.org/packages/b8/fc/4c2fb705f6371cb83824da45a8b967514a922fc092a0ef53979334d97a70/time_machine-2.19.0-cp314-cp314-win_arm64.whl", hash = "sha256:f379c6f8a6575a8284592179cf528ce89373f060301323edcc44f1fa1d37be12", size = 16752, upload-time = "2025-08-19T17:21:42.336Z" }, + { url = "https://files.pythonhosted.org/packages/79/ab/6437d18f31c666b5116c97572a282ac2590a82a0a9867746a6647eaf4613/time_machine-2.19.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:a3b8981f9c663b0906b05ab4d0ca211fae4b63b47c6ec26de5374fe56c836162", size = 20057, upload-time = "2025-08-19T17:21:43.35Z" }, + { url = "https://files.pythonhosted.org/packages/6c/a2/e03639ec2ba7200328bbcad8a2b2b1d5fccca9cceb9481b164a1cabdcb33/time_machine-2.19.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:8e9c6363893e7f52c226afbebb23e825259222d100e67dfd24c8a6d35f1a1907", size = 15430, upload-time = "2025-08-19T17:21:44.725Z" }, + { url = "https://files.pythonhosted.org/packages/5d/ff/39e63a48e840f3e36ce24846ee51dd99c6dba635659b1750a2993771e88e/time_machine-2.19.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:206fcd6c9a6f00cac83db446ad1effc530a8cec244d2780af62db3a2d0a9871b", size = 39622, upload-time = "2025-08-19T17:21:45.821Z" }, + { url = "https://files.pythonhosted.org/packages/9a/2e/ee5ac79c4954768705801e54817c7d58e07e25a0bb227e775f501f3e2122/time_machine-2.19.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bf33016a1403c123373ffaeff25e26e69d63bf2c63b6163932efed94160db7ef", size = 41235, upload-time = "2025-08-19T17:21:46.783Z" }, + { url = "https://files.pythonhosted.org/packages/3a/3e/9af5f39525e779185c77285b8bbae15340eeeaa0afb33d458bc8b47d459b/time_machine-2.19.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9247c4bb9bbd3ff584ef4efbdec8efd9f37aa08bcfc4728bde1e489c2cb445bd", size = 44276, upload-time = "2025-08-19T17:21:47.759Z" }, + { url = "https://files.pythonhosted.org/packages/59/fe/572c7443cc27140bbeae3947279bbd4a120f9e8622253a20637f260b7813/time_machine-2.19.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:77f9bb0b86758d1f2d9352642c874946ad5815df53ef4ca22eb9d532179fe50d", size = 42330, upload-time = "2025-08-19T17:21:48.881Z" }, + { url = "https://files.pythonhosted.org/packages/cf/24/1a81c2e08ee7dae13ec8ceed27a29afa980c3d63852e42f1e023bf0faa03/time_machine-2.19.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:0b529e262df3b9c449f427385f4d98250828c879168c2e00eec844439f40b370", size = 39281, upload-time = "2025-08-19T17:21:49.907Z" }, + { url = "https://files.pythonhosted.org/packages/d2/60/6f0d6e5108978ca1a2a4ffb4d1c7e176d9199bb109fd44efe2680c60b52a/time_machine-2.19.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9199246e31cdc810e5d89cb71d09144c4d745960fdb0824da4994d152aca3303", size = 40201, upload-time = "2025-08-19T17:21:50.953Z" }, + { url = "https://files.pythonhosted.org/packages/73/b9/3ea4951e8293b0643feb98c0b9a176fa822154f1810835db3f282968ab10/time_machine-2.19.0-cp314-cp314t-win32.whl", hash = "sha256:0fe81bae55b7aefc2c2a34eb552aa82e6c61a86b3353a3c70df79b9698cb02ca", size = 17743, upload-time = "2025-08-19T17:21:51.948Z" }, + { url = "https://files.pythonhosted.org/packages/e4/8b/cd802884ca8a98e2b6cdc2397d57dd12ff8a7d1481e06fc3fad3d4e7e5ff/time_machine-2.19.0-cp314-cp314t-win_amd64.whl", hash = "sha256:7253791b8d7e7399fbeed7a8193cb01bc004242864306288797056badbdaf80b", size = 18956, upload-time = "2025-08-19T17:21:52.997Z" }, + { url = "https://files.pythonhosted.org/packages/c6/49/cabb1593896082fd55e34768029b8b0ca23c9be8b2dc127e0fc14796d33e/time_machine-2.19.0-cp314-cp314t-win_arm64.whl", hash = "sha256:536bd1ac31ab06a1522e7bf287602188f502dc19d122b1502c4f60b1e8efac79", size = 17068, upload-time = "2025-08-19T17:21:54.064Z" }, + { url = "https://files.pythonhosted.org/packages/d6/05/0608376c3167afe6cf7cdfd2b05c142ea4c42616eee9ba06d1799965806a/time_machine-2.19.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:d8bb00b30ec9fe56d01e9812df1ffe39f331437cef9bfaebcc81c83f7f8f8ee2", size = 19659, upload-time = "2025-08-19T17:21:55.426Z" }, + { url = "https://files.pythonhosted.org/packages/11/c4/72eb8c7b36830cf36c51d7bc2f1ac313d68881c3a58040fb6b42c4523d20/time_machine-2.19.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d821c60efc08a97cc11e5482798e6fd5eba5c0f22a02db246b50895dbdc0de41", size = 15153, upload-time = "2025-08-19T17:21:56.505Z" }, + { url = "https://files.pythonhosted.org/packages/89/1a/0782e1f5c8ab8809ebd992709e1bb69d67600191baa023af7a5d32023a3c/time_machine-2.19.0-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:fb051aec7b3b6e96a200d911c225901e6133ff3da11e470e24111a53bbc13637", size = 32555, upload-time = "2025-08-19T17:21:57.74Z" }, + { url = "https://files.pythonhosted.org/packages/94/b0/8ef58e2f6321851d5900ca3d18044938832c2ed42a2ac7570ca6aa29768a/time_machine-2.19.0-cp39-cp39-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fe59909d95a2ef5e01ce3354fdea3908404c2932c2069f00f66dff6f27e9363e", size = 34185, upload-time = "2025-08-19T17:21:59.361Z" }, + { url = "https://files.pythonhosted.org/packages/82/74/ce0c9867f788c1fb22c417ec1aae47a24117e53d51f6ff97d7c6ca5392f6/time_machine-2.19.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:29e84b8682645b16eb6f9e8ec11c35324ad091841a11cf4fc3fc7f6119094c89", size = 35917, upload-time = "2025-08-19T17:22:00.421Z" }, + { url = "https://files.pythonhosted.org/packages/d2/70/6f97a8f552dbaa66feb10170b5726dab74bc531673d1ed9d6f271547e54c/time_machine-2.19.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:4a11f1c0e0d06023dc01614c964e256138913551d3ae6dca5148f79081156336", size = 34584, upload-time = "2025-08-19T17:22:01.447Z" }, + { url = "https://files.pythonhosted.org/packages/48/c8/cf139088ce537c15d7f03cf56ec317d3a5cfb520e30aa711ea0248d0ae8a/time_machine-2.19.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:57a235a6307c54df50e69f1906e2f199e47da91bde4b886ee05aff57fe4b6bf6", size = 32608, upload-time = "2025-08-19T17:22:02.548Z" }, + { url = "https://files.pythonhosted.org/packages/b1/17/0ec41ef7a30c6753fb226a28b74162b264b35724905ced4098f2f5076ded/time_machine-2.19.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:426aba552f7af9604adad9ef570c859af7c1081d878db78089fac159cd911b0a", size = 33686, upload-time = "2025-08-19T17:22:03.606Z" }, + { url = "https://files.pythonhosted.org/packages/b0/19/586f15159083ec84f178d494c60758c46603b00c9641b04deb63f1950128/time_machine-2.19.0-cp39-cp39-win32.whl", hash = "sha256:67772c7197a3a712d1b970ed545c6e98db73524bd90e245fd3c8fa7ad7630768", size = 17133, upload-time = "2025-08-19T17:22:04.989Z" }, + { url = "https://files.pythonhosted.org/packages/6a/c2/bfe4b906a9fe0bf2d011534314212ed752d6b8f392c9c82f6ac63dccc5ab/time_machine-2.19.0-cp39-cp39-win_amd64.whl", hash = "sha256:011d7859089263204dc5fdf83dce7388f986fe833c9381d6106b4edfda2ebd3e", size = 17972, upload-time = "2025-08-19T17:22:06.026Z" }, + { url = "https://files.pythonhosted.org/packages/5d/73/182343eba05aa5787732aaa68f3b3feb5e40ddf86b928ae941be45646393/time_machine-2.19.0-cp39-cp39-win_arm64.whl", hash = "sha256:e1af66550fa4685434f00002808a525f176f1f92746646c0019bb86fbff48b27", size = 16820, upload-time = "2025-08-19T17:22:07.227Z" }, +] + +[[package]] +name = "time-machine" +version = "3.1.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and extra != 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2'", + "python_full_version >= '3.10' and python_full_version < '3.14' and extra != 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2'", + "python_full_version >= '3.10' and extra == 'group-9-stagehand-pydantic-v1' and extra != 'group-9-stagehand-pydantic-v2'", + "python_full_version >= '3.10' and extra != 'group-9-stagehand-pydantic-v1' and extra != 'group-9-stagehand-pydantic-v2'", +] +sdist = { url = "https://files.pythonhosted.org/packages/17/bd/a1bb03eb39ce35c966f0dde6559df7348ca0580f7cd3a956fdd7ed0b5080/time_machine-3.1.0.tar.gz", hash = "sha256:90831c2cf9e18e4199abb85fafa0c0ca0c6c15d0894a03ef68d5005a796c4f27", size = 14436, upload-time = "2025-11-21T13:56:33.802Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0a/c9/0aaa082d6b5c489c22d9db025cbf17016062e953f70a7678d76b520f274f/time_machine-3.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e867178cc87490c578534832c29f048cc954b32a01681237e52ccda704baece5", size = 19046, upload-time = "2025-11-21T13:54:53.273Z" }, + { url = "https://files.pythonhosted.org/packages/30/0d/e825251028c68822a63478c7e44a0dca640daedb15f685a9a3973edf8ae8/time_machine-3.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5d4d4b944e0197c7726844f452fcb3dc17991165e9b4fab779e505b095bb7363", size = 15027, upload-time = "2025-11-21T13:54:54.474Z" }, + { url = "https://files.pythonhosted.org/packages/01/ee/d1e041f85787b835ed70bc7c31000a03d197f831490304f2527e9b840507/time_machine-3.1.0-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9fc81014e6fc989b833e8cae3684b78b1e1f7c0d867c2fbfea785385a806ea6b", size = 32548, upload-time = "2025-11-21T13:54:55.868Z" }, + { url = "https://files.pythonhosted.org/packages/50/c0/12fc339c054e0db1da79c476bb1c3cc0b8796789e90eb8687af96bbdde9b/time_machine-3.1.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:aebd2a55f860b1ef28a2c1efe81454a6fa110ec1a6517c011760132f3cbf35fa", size = 34150, upload-time = "2025-11-21T13:54:56.955Z" }, + { url = "https://files.pythonhosted.org/packages/84/ab/40a2d31d4f742e41fc7f3703da672bbc25b505df3e7ab5df6c11a39e435e/time_machine-3.1.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:02e8018629e12e8064fee4f8fbbb8ae353f5907051965b5721ef189aeb7f833c", size = 35694, upload-time = "2025-11-21T13:54:58.053Z" }, + { url = "https://files.pythonhosted.org/packages/6f/1f/f31b604cb72af2c89311e8152bfe4e64a890785daeb19939bb841ed4cb77/time_machine-3.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:2fbbcf9de9a1d3e94e8b6c41ac7e9da46948fbdf489dbc083ea6c28ed045a43a", size = 34500, upload-time = "2025-11-21T13:54:59.428Z" }, + { url = "https://files.pythonhosted.org/packages/4f/1d/dca59c5d54dd0777b342fa708ffb24e0c595b8c47106300bc154dbaa8d98/time_machine-3.1.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:358f0feead47ee74c7747cb5b7f73582cbde1c033c20f9350e58ab4389aa59ff", size = 32605, upload-time = "2025-11-21T13:55:00.877Z" }, + { url = "https://files.pythonhosted.org/packages/f0/af/033b7b29c9364f05e99b4f35152574edc21b2f038589dafd60a60945a017/time_machine-3.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f6d3f9e531127306610433fe62368b9e14621e336da2042d062565d655d97a02", size = 33699, upload-time = "2025-11-21T13:55:02.287Z" }, + { url = "https://files.pythonhosted.org/packages/4e/92/782c75e9f958e2cb33ba30f9a7adeb819811626eb3193e5ade4343ef197d/time_machine-3.1.0-cp310-cp310-win32.whl", hash = "sha256:bd4b4279938472ea18e5580330c10f8d49b8aec34e0df71be46e3be3b0f03f1d", size = 17054, upload-time = "2025-11-21T13:55:04.036Z" }, + { url = "https://files.pythonhosted.org/packages/ce/e6/cbdb32a72d2e122646cd3c62aed47dcddb366196798caa39043985d4e11d/time_machine-3.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:10a0d18b65af008e1cb60e0cc57594e67d3bbfee07204879f0e5864223dfd899", size = 17898, upload-time = "2025-11-21T13:55:05.069Z" }, + { url = "https://files.pythonhosted.org/packages/1f/d2/3663703fc694b07241673391c267629f881d4c025c392d7df09161031ac7/time_machine-3.1.0-cp310-cp310-win_arm64.whl", hash = "sha256:53702d6e594fc395b66517bc75c3d2ad8bfbff1f693d09bb67f8178cdfd21cd5", size = 16630, upload-time = "2025-11-21T13:55:06.178Z" }, + { url = "https://files.pythonhosted.org/packages/67/2b/9f5cea745e6c704cbbd1b6c36e0c73ca3204160e9c79234d66f140b326f6/time_machine-3.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:3b205f91d3907f6bd1747fe5484a3ed931e121e51fec32e4d6a8ee6eb41c37c3", size = 18741, upload-time = "2025-11-21T13:55:07.288Z" }, + { url = "https://files.pythonhosted.org/packages/76/17/7b5d94a119883b56b446980387e8ab83f37037db01696cf236cbc85807dc/time_machine-3.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7004f2fd396b6d40cbf7c21df82f838445d899a3ed2ecc5b1fb67eea7e3d2fa8", size = 14865, upload-time = "2025-11-21T13:55:08.304Z" }, + { url = "https://files.pythonhosted.org/packages/2c/1e/a6ff6587d520ac20bf7414faf8f53cf094dd9fe450acf3b0c85e0b332c8a/time_machine-3.1.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:68c35b3450c27cf0087c90ae872ab41ec8097b76b5fb6b07321fc5873e78f152", size = 30643, upload-time = "2025-11-21T13:55:09.8Z" }, + { url = "https://files.pythonhosted.org/packages/45/50/dcf4272d7f9a4690d9edd983b5690efa8df3cc7671ade9bcf3439adac278/time_machine-3.1.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2e28aed9da0182c10cb0344aa06fac585f394768c7d088bee781ad2779ea6fe0", size = 32226, upload-time = "2025-11-21T13:55:10.927Z" }, + { url = "https://files.pythonhosted.org/packages/dc/7c/b719ae736568b3f2c9bf8d3bc65bada96b04c9241c628fcb5ab0724a6928/time_machine-3.1.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9d7faced22388578dbed3b4be0603eb00f709857cd57b7b9738cd81fbaf326a9", size = 33883, upload-time = "2025-11-21T13:55:12.32Z" }, + { url = "https://files.pythonhosted.org/packages/e5/5c/f433fe44eadecfe21e4f2bf128d240c15d295592c877490a475960644281/time_machine-3.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2e8a78dd7afc3a081e208e0b8d1a8105cff95d96e4f79da746afdf05fb822e7a", size = 32769, upload-time = "2025-11-21T13:55:13.541Z" }, + { url = "https://files.pythonhosted.org/packages/09/0f/fbc0e1437801315d4bdd8c21f9ef0c51f005a327ab0289ca034658fe78a1/time_machine-3.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8b9dce925dae420d6b66dec4cae00785260dbededec5c89eaedbfba99a2be55b", size = 30765, upload-time = "2025-11-21T13:55:14.706Z" }, + { url = "https://files.pythonhosted.org/packages/3f/c0/87d23d5817d95ed07fe272bb7a5de91177bb18274718d318c73a4aa0a4c2/time_machine-3.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:17f0d30a05073cdee68162779870eadf5e7f091bc94ae96d90d8fddbb8020714", size = 31885, upload-time = "2025-11-21T13:55:16.56Z" }, + { url = "https://files.pythonhosted.org/packages/4f/d7/572e38dadab9efe6ec4fff6e063f488866121dc384873d5b04fc5855ca83/time_machine-3.1.0-cp311-cp311-win32.whl", hash = "sha256:9e836e4fa8cb58de80de863335f4566f896b4dcd69d8a400d705857ca8301872", size = 16935, upload-time = "2025-11-21T13:55:17.612Z" }, + { url = "https://files.pythonhosted.org/packages/00/a6/edc968e1429a14d28676adb596f42570aa42def63014ccd3ccaf8d279d43/time_machine-3.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:1d652f85cbd2fd41e4262c27515023cc216589ca0b4aebab458972cce8119cc1", size = 17779, upload-time = "2025-11-21T13:55:18.668Z" }, + { url = "https://files.pythonhosted.org/packages/a0/97/2025eea7792f1be50777d85a2e2974d4416698c0002c419a61fcc6222de8/time_machine-3.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:4be520b8ed752e788f57f72617f12b5bf5103e3db2b062e69b82e7f7f2977c4f", size = 16496, upload-time = "2025-11-21T13:55:19.753Z" }, + { url = "https://files.pythonhosted.org/packages/39/3d/412015d3e2f682548b7222367aa8d7b91d323145234d216847bc56c2d720/time_machine-3.1.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d3d6606ae81a29c925452f8b56115936aeda34c0a25d40af53cf1b29c66235ef", size = 18851, upload-time = "2025-11-21T13:55:21Z" }, + { url = "https://files.pythonhosted.org/packages/33/98/48f60552570d6d66773dcfbc1d7b8fb305d3e9ae0694dd249f1ae0bc3b77/time_machine-3.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0265df307778104c44d9114b55246a5b99da03f1dcb791305f9af21b0389ef7b", size = 14955, upload-time = "2025-11-21T13:55:22.408Z" }, + { url = "https://files.pythonhosted.org/packages/4d/7a/6fd1c47d3a1d87919d38f85c12db8f838298acb4ca3d6908f3288bcea0fd/time_machine-3.1.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:098da08900bdf6c6bd44b36ec06afd5b92c7a7140cd48c8686a79b6c6fef9da7", size = 32622, upload-time = "2025-11-21T13:55:23.541Z" }, + { url = "https://files.pythonhosted.org/packages/64/74/01641bd3d5f8c4f22710b7070d1dbeaeb501e8549e37419fc8b995bead32/time_machine-3.1.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:092f2e7a2526a02fcbd9c0a4165e0790350a13da4b01b6306b82e9580d83ae69", size = 34172, upload-time = "2025-11-21T13:55:24.638Z" }, + { url = "https://files.pythonhosted.org/packages/35/df/91f39b8bfe42c67dd3e66d8d2baa2c1b10126cc6e217fb3c7b1e777804c5/time_machine-3.1.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b64b6cada6dd2433cdaeda53dd940bdd579e40a8c92c5379527694570bb58b97", size = 35554, upload-time = "2025-11-21T13:55:25.841Z" }, + { url = "https://files.pythonhosted.org/packages/b0/6c/236434de77561003429e90300327b5ac6a6eeaa6d6c967282d28d1983232/time_machine-3.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2776e3300ef499541f9081b4a03ff1b3e7681e51a594572e1bf191773504bd21", size = 34261, upload-time = "2025-11-21T13:55:27.715Z" }, + { url = "https://files.pythonhosted.org/packages/97/76/32eea75715aefbd7ccfeea70285bb5400ecebd8dc3524b9c3491115e2504/time_machine-3.1.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:86e10914592a95e35edeb081975b6527919300bd1b65c04ee7f765db7bf2c1ad", size = 32485, upload-time = "2025-11-21T13:55:29.55Z" }, + { url = "https://files.pythonhosted.org/packages/b0/1b/91c529de2d2c6d7097692b4ae620cbe30bf24a4609d737b5f41d91a77bb0/time_machine-3.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c1362a6672252fb0d79da492dcf75eb7369b37fe595946ee3c2848df2fcc22e7", size = 33674, upload-time = "2025-11-21T13:55:30.636Z" }, + { url = "https://files.pythonhosted.org/packages/d6/af/345dfab6543e79151867daabbc4f4788ee10e408b8bd1361d066d0fea932/time_machine-3.1.0-cp312-cp312-win32.whl", hash = "sha256:50773648c69960e6e8497077875427aeb484d6a57a06399502cc125e349fca19", size = 16962, upload-time = "2025-11-21T13:55:31.73Z" }, + { url = "https://files.pythonhosted.org/packages/7c/9d/79a4ebed58d78cad85a5fc5c4ed4558d4d018d8a2bb7858ea02704b49be7/time_machine-3.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:20e0974e58a40a626d353d6132b7595de3fcb8deb72da4a762071b315cc95f6f", size = 17723, upload-time = "2025-11-21T13:55:32.76Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a2/dd405133427dc47afd95618c3519854147408ed05deb209ba1b6b704689b/time_machine-3.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:da9bced71b5966312f13c423b5b124649f1b08a9400d95018eb6d23311b384b9", size = 16520, upload-time = "2025-11-21T13:55:33.972Z" }, + { url = "https://files.pythonhosted.org/packages/c7/27/8af0187b4f7c574d7c4d7e86dbaece47ac92666fda8717c787849bc48560/time_machine-3.1.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:99d91fc0d4d91be1e1ea92389e1e93b0f43bf60433409616cb43de69be6505a8", size = 18911, upload-time = "2025-11-21T13:55:35.056Z" }, + { url = "https://files.pythonhosted.org/packages/92/35/a948a07659d471be160c8500c2e82ca0576a067d52d86ebe7ef24ea8e141/time_machine-3.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ff18d291b7e681af3dc2f069a182a367baee510ab9800120a487a01d2ec929a9", size = 14983, upload-time = "2025-11-21T13:55:36.414Z" }, + { url = "https://files.pythonhosted.org/packages/4a/8c/0cc16dd1d058580c00ffa685401756bd6170efe4434d418b724e96e3a0ac/time_machine-3.1.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:8fedc78cab733dfe1afeec09bd841aea314249b316eb02f17ad2e617c400fa4d", size = 32709, upload-time = "2025-11-21T13:55:37.628Z" }, + { url = "https://files.pythonhosted.org/packages/20/34/f2f162c67854be20c34ed9c49474b6abd6427108b98c3452533e60ba2526/time_machine-3.1.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:62bb78376c4a13a0463b71bc9ffd81d9e3e6ed9efdbe500716d9c51ae5a2a60c", size = 34299, upload-time = "2025-11-21T13:55:38.999Z" }, + { url = "https://files.pythonhosted.org/packages/8b/2c/314fe33e24bbc46837643d5add7a9843c7cbd4b66f355a94e98c700ddcac/time_machine-3.1.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bc750d300bd8794a0581360632e20dd5896d21b5c1c6b74c7c01c72bebd65df2", size = 35699, upload-time = "2025-11-21T13:55:40.493Z" }, + { url = "https://files.pythonhosted.org/packages/ac/28/c2f5d93024ef712897352e3fb801425325adfb3b2c33d3ba7838c8ea5941/time_machine-3.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d23623c3223318cb4b43ad07e10b682821c59b6ab99cce3d9db0f80bc2206ead", size = 34359, upload-time = "2025-11-21T13:55:41.672Z" }, + { url = "https://files.pythonhosted.org/packages/25/c6/67a6abd6ab75a6c16275cd8b5bf13053f33fac1de83a5b8e569685d37005/time_machine-3.1.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:48a4ad44202dcafd302310739e086374c8e928d6604bb5812b1f5e755dbcd5e1", size = 32604, upload-time = "2025-11-21T13:55:42.878Z" }, + { url = "https://files.pythonhosted.org/packages/13/c7/1277ebfbcfaea02bbf01a69beac821a6543e1829a47bda52a020b3509ba2/time_machine-3.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8b6f8572c1e674d5ae43882ee9f39a03cea86c31cf9904ff3767905d41fc5e5a", size = 33779, upload-time = "2025-11-21T13:55:44.057Z" }, + { url = "https://files.pythonhosted.org/packages/6b/39/773a7456222b391e2f0bc6d049e00d99ea78b1e226b8b36c261e1235f36d/time_machine-3.1.0-cp313-cp313-win32.whl", hash = "sha256:8743edd11d3e2cb2d0244d4e83d96873fd96a375ba75364399f2f64fd95c7ec4", size = 16984, upload-time = "2025-11-21T13:55:45.144Z" }, + { url = "https://files.pythonhosted.org/packages/e9/95/94b9a839586eae1e3afcd575d1dabf81929e44e3886ad6d94deb5e2d5bda/time_machine-3.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:3a99c68a1cc962f76709c2b67efdcf4c97c9ad4a950f694cccb413ab378f9d94", size = 17727, upload-time = "2025-11-21T13:55:46.524Z" }, + { url = "https://files.pythonhosted.org/packages/1f/fd/f1fb569e8c7547c983b4e3259ee40684b0c4fdc882f36864d5eb05d71f72/time_machine-3.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:7be2af8b69892615d897b2c6b546093e45dba7d9cde6af64c17f1e5da4f38dbc", size = 16558, upload-time = "2025-11-21T13:55:47.922Z" }, + { url = "https://files.pythonhosted.org/packages/e9/15/ccb1178e3a0988c320075285fe7b5ab26e51b71b2e5e14eee7158bd04dd6/time_machine-3.1.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:56938d4a24435014d3d9141415aee81699cf1a5419462a24357c7e3181c67f06", size = 19593, upload-time = "2025-11-21T13:55:50.114Z" }, + { url = "https://files.pythonhosted.org/packages/6a/9e/495e1ae27227cc3fc20f5d9e9011c14a3bda515f0c98630b0d0e2c444c4a/time_machine-3.1.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:ecdd9b98305cecfbff29ad9b663f38cbcf728ff023dc1db63cc94bd439890da5", size = 15252, upload-time = "2025-11-21T13:55:51.213Z" }, + { url = "https://files.pythonhosted.org/packages/d6/03/c905659d81c6b071cd8f2a6a6a23b1e25cd2a498167125b95e543fea7cec/time_machine-3.1.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e5f0dc8ba33fcd624d11dc4029fa3fd1712f96660ddc629ce61097c71d8f6400", size = 38810, upload-time = "2025-11-21T13:55:52.379Z" }, + { url = "https://files.pythonhosted.org/packages/42/26/393277a6f07472cdb56ee2d8e34f0bdc203f64c8857180b73c4ba9cf0d91/time_machine-3.1.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:812aad79acf4b3471d997d29a5e7010f0a914740b1fe5b6cefb81b462cb28824", size = 40358, upload-time = "2025-11-21T13:55:54.077Z" }, + { url = "https://files.pythonhosted.org/packages/86/70/0cc738ba7fdaf8d29acd128a124be00c781b33e3ea84f34211f5a2cff4c2/time_machine-3.1.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5b0218aa05865a16c000320cfdac921d0e02992ef51e711325bc366bacdc4aeb", size = 43118, upload-time = "2025-11-21T13:55:55.616Z" }, + { url = "https://files.pythonhosted.org/packages/dc/5a/6c42a046abfcb8996ef3239bbc1cfd7c0051dea166a0f9f01923d1eb1848/time_machine-3.1.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:3544ab394f4149e8884996f1c5047d52dbb08bb2396109c530eee6ecffd6f4c4", size = 41321, upload-time = "2025-11-21T13:55:56.869Z" }, + { url = "https://files.pythonhosted.org/packages/36/3e/1123a93add930d4933ca4f1c3441f1832eba6b9e1b41b9ca3a5d3f9203c7/time_machine-3.1.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:acf41d44e526cae2f62c9c6ac95daef42bdbd7d95bdb3bb60e071b4b61110723", size = 38547, upload-time = "2025-11-21T13:55:58.065Z" }, + { url = "https://files.pythonhosted.org/packages/d1/c8/c98855aa75a6bc49c352e00396e545353db3e5d7c65a6eefca76366d9aac/time_machine-3.1.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:bb7031a367209c223c41ab625172b38362e0ce07f13f1f1e7d75d5194fcdd0d7", size = 39421, upload-time = "2025-11-21T13:55:59.224Z" }, + { url = "https://files.pythonhosted.org/packages/aa/94/f1520be4f125489a9d327848048688c2c13c5705770b98caac63e35cc204/time_machine-3.1.0-cp313-cp313t-win32.whl", hash = "sha256:ecf49c418f854f42171b4f0859906a26ff56d73303dee2e83beb307747e11db1", size = 17436, upload-time = "2025-11-21T13:56:00.395Z" }, + { url = "https://files.pythonhosted.org/packages/be/2f/79e13c341b20e8ceb1629fb2e1ae36063c9dee42f3886be44a54867ad0dc/time_machine-3.1.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ee93cf4c11452bf8211bf12a926d6f5179c241558f6af30c2de2669bf26ba1c1", size = 18505, upload-time = "2025-11-21T13:56:01.54Z" }, + { url = "https://files.pythonhosted.org/packages/72/42/0cdb0b67d44ebfa47f4dbecb65d25522312ee772f59c4d63a0df0c895f34/time_machine-3.1.0-cp313-cp313t-win_arm64.whl", hash = "sha256:9c317eebaa0578a370a29f40152db0ac00bd34b387b54c95bf01fd123bca178d", size = 16852, upload-time = "2025-11-21T13:56:02.977Z" }, + { url = "https://files.pythonhosted.org/packages/f0/14/2f9b4c6ae63662827c48d81c445fedeba4733248a56640747c8e5be55870/time_machine-3.1.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:2b07055e5327e04d725557a07a69523d14d2d897877d90781b9c27c70bd8997c", size = 18899, upload-time = "2025-11-21T13:56:04.186Z" }, + { url = "https://files.pythonhosted.org/packages/6a/c2/09a91825ea60413316ece41e448d275a9a4b719bc92b35b6166013dc01bb/time_machine-3.1.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:b71948998e3f709bda9b600b0d250bb4ad677b28fac32475b6093aa5b9e8969f", size = 15027, upload-time = "2025-11-21T13:56:05.299Z" }, + { url = "https://files.pythonhosted.org/packages/f5/65/b737258b39b98406a3ed681cdc025fa788441221c5d24a59897a4752e413/time_machine-3.1.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:343dfb0663ccb1e5d5dc6dfb651b7b7233985c73b3a3f6af0fe58c9cf5b0f4ab", size = 32798, upload-time = "2025-11-21T13:56:06.521Z" }, + { url = "https://files.pythonhosted.org/packages/e6/e0/063edd2188a5c7e8f4b1a184dc9e87de955dcfd5cd8f706131739ff0685c/time_machine-3.1.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3df9b834ec2ee8536a398c15c70f5d54dfe3bbb34344f6549ba29acf80916948", size = 34385, upload-time = "2025-11-21T13:56:07.719Z" }, + { url = "https://files.pythonhosted.org/packages/84/e8/ead05dc304f973b01443829367be3c504f3ff86c394a3fec932c4d720f3f/time_machine-3.1.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9e1ee0725a016f69fa8f0f37d793ba6d2d3870b32e164650a6922caf065f2ce2", size = 35781, upload-time = "2025-11-21T13:56:08.931Z" }, + { url = "https://files.pythonhosted.org/packages/1b/5e/23303d6b13482436d6c37015d17142821adf8e47c1104c0a4c5fc0bdb173/time_machine-3.1.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ed552f135efabca895e678044ce1dbb693e6a399003606e9d6a413b2eaf48a51", size = 34447, upload-time = "2025-11-21T13:56:10.468Z" }, + { url = "https://files.pythonhosted.org/packages/30/be/5ace5c8e2dc0b6899c3e18ebf4301211a50e1addfcbecbf61a100a76ac03/time_machine-3.1.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:c93451242de81fe2a96f699dad97aa463250688b10d2e8a72e98208df9bd62b1", size = 32814, upload-time = "2025-11-21T13:56:12.072Z" }, + { url = "https://files.pythonhosted.org/packages/40/cb/b0212b98e422fbb08f7328aabe0c6f59e853146eb61337df8f497dd4a2ad/time_machine-3.1.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:010c2dd1f084eae4687021f7b7fd798abc7a8472f2e783919aafe7b8fe624c8b", size = 33864, upload-time = "2025-11-21T13:56:13.28Z" }, + { url = "https://files.pythonhosted.org/packages/28/53/08ad68c1971257ee0c9b9ec38b1f2ef88ae7565e0c7eb272f9ca3ff40152/time_machine-3.1.0-cp314-cp314-win32.whl", hash = "sha256:9b8e24de4ba47401dcec53733d98db9678a708f6bafb77a64e46636304eca64c", size = 17127, upload-time = "2025-11-21T13:56:14.414Z" }, + { url = "https://files.pythonhosted.org/packages/0c/36/95d5b7fff7e1506f9f4a481df3b17ebae0f3ab4a36669e6a93890df1da5f/time_machine-3.1.0-cp314-cp314-win_amd64.whl", hash = "sha256:6873c903c8de85884655afc49c8465136ea5d6c7500ad2bea31601cf6a48939f", size = 18006, upload-time = "2025-11-21T13:56:15.575Z" }, + { url = "https://files.pythonhosted.org/packages/d2/c8/b30211e36117b4436368927e46dcf1f785626069b11a12cc3ea150337136/time_machine-3.1.0-cp314-cp314-win_arm64.whl", hash = "sha256:9a74b2da34e7e6aaa2db011556f40f8ea26e89a3a1683ffad43ceca1789b8af0", size = 16633, upload-time = "2025-11-21T13:56:16.706Z" }, + { url = "https://files.pythonhosted.org/packages/fe/3d/eeb00cd285102e39cc4eeeb4e78cc1fcff8a89691bdc6a708d4c40fe38cc/time_machine-3.1.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:0c688897189b50f0820e2916579c7e88aefef2a86cd17aa05b5b7a6676dbd97e", size = 19578, upload-time = "2025-11-21T13:56:17.853Z" }, + { url = "https://files.pythonhosted.org/packages/3e/91/30710e1883a4c39b1367ef469d6fd18c791bec4ee8783a19af9ac82bc632/time_machine-3.1.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:1715d221c4c49bd2a0bc73868d5543133ab15e02e0d9726d73d802ccf978e1c0", size = 15299, upload-time = "2025-11-21T13:56:18.955Z" }, + { url = "https://files.pythonhosted.org/packages/d4/7f/2311774df6d41dba3934494b6589195a726fec0753a4e8e8eba28e509327/time_machine-3.1.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:8cecc83df46ab1095f93ff42dceaddb313e12efd9135cd153d0021a98b570390", size = 38801, upload-time = "2025-11-21T13:56:20.357Z" }, + { url = "https://files.pythonhosted.org/packages/1b/74/5af7e7af3787333c927d860476b505ec8770412e2bb1ba4e2d00a3aa644a/time_machine-3.1.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:be63019454e0e30138bfe414f3dd5f626d32b8a265ea99bdc4b107867b68128a", size = 40357, upload-time = "2025-11-21T13:56:21.971Z" }, + { url = "https://files.pythonhosted.org/packages/10/1a/ebcecff1e57f52788989f0734a57eab5e045c9784cfd998040b8ba280f5b/time_machine-3.1.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ac1245d6a4a4ac40e308362e4236c1aad6ead836c97576c7e29167752a5283d0", size = 43101, upload-time = "2025-11-21T13:56:23.555Z" }, + { url = "https://files.pythonhosted.org/packages/99/b3/63883e2d8555358469da098dd1568ec8f6c9b6d7317796cfbf8bc5c59ab2/time_machine-3.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:9fde90d14b13396ecaa5ce2366f788f7c01b7bf4ac4246b798c622bc6369b861", size = 41305, upload-time = "2025-11-21T13:56:24.831Z" }, + { url = "https://files.pythonhosted.org/packages/04/a3/d7851676cf7a5d5451b73f271b6b7229688f403488a8dd111b5fe5fde7cf/time_machine-3.1.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:15d83cc338d02dc6e674e24ce40a8c311d75191c86014b0be455a4267f27f00e", size = 38534, upload-time = "2025-11-21T13:56:26.032Z" }, + { url = "https://files.pythonhosted.org/packages/13/dc/ced9245bc633f0c4790a57b3c6089a586f0a208b50f8ec7d001bf8254d49/time_machine-3.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cbbd90494a9bc6626e3180594246cb9557418e32f2cb2c40edf8526a182f5e31", size = 39430, upload-time = "2025-11-21T13:56:28.83Z" }, + { url = "https://files.pythonhosted.org/packages/67/24/bb20ff76ed4e8e09ab65910bf21a315dc7562c8be92250363b25f3ab1dd1/time_machine-3.1.0-cp314-cp314t-win32.whl", hash = "sha256:6c00758d155601d710fa036c8d24d5ad3fb28531933cf70343006cf2be93092a", size = 17674, upload-time = "2025-11-21T13:56:29.969Z" }, + { url = "https://files.pythonhosted.org/packages/2e/64/42573a6da9298efd68a831d4e9eabc8c9c0cac9305bc19bb24a4066bbba0/time_machine-3.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:e00239b54b05255862e0965e6ae67728e467727ca7dc23d9a6c5a51c7b5b01c8", size = 18792, upload-time = "2025-11-21T13:56:31.123Z" }, + { url = "https://files.pythonhosted.org/packages/35/10/09ad4e5ccc27224ed8377a6f3d191034242c404d0c1ad5f119d79fb18363/time_machine-3.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:8b47da89595dc3c7f0b52f1e3f3f8da8325037f3746e66d74bebac9f42f2a989", size = 16944, upload-time = "2025-11-21T13:56:32.254Z" }, +] + +[[package]] +name = "tomli" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/52/ed/3f73f72945444548f33eba9a87fc7a6e969915e7b1acc8260b30e1f76a2f/tomli-2.3.0.tar.gz", hash = "sha256:64be704a875d2a59753d80ee8a533c3fe183e3f06807ff7dc2232938ccb01549", size = 17392, upload-time = "2025-10-08T22:01:47.119Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/2e/299f62b401438d5fe1624119c723f5d877acc86a4c2492da405626665f12/tomli-2.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:88bd15eb972f3664f5ed4b57c1634a97153b4bac4479dcb6a495f41921eb7f45", size = 153236, upload-time = "2025-10-08T22:01:00.137Z" }, + { url = "https://files.pythonhosted.org/packages/86/7f/d8fffe6a7aefdb61bced88fcb5e280cfd71e08939da5894161bd71bea022/tomli-2.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:883b1c0d6398a6a9d29b508c331fa56adbcdff647f6ace4dfca0f50e90dfd0ba", size = 148084, upload-time = "2025-10-08T22:01:01.63Z" }, + { url = "https://files.pythonhosted.org/packages/47/5c/24935fb6a2ee63e86d80e4d3b58b222dafaf438c416752c8b58537c8b89a/tomli-2.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d1381caf13ab9f300e30dd8feadb3de072aeb86f1d34a8569453ff32a7dea4bf", size = 234832, upload-time = "2025-10-08T22:01:02.543Z" }, + { url = "https://files.pythonhosted.org/packages/89/da/75dfd804fc11e6612846758a23f13271b76d577e299592b4371a4ca4cd09/tomli-2.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a0e285d2649b78c0d9027570d4da3425bdb49830a6156121360b3f8511ea3441", size = 242052, upload-time = "2025-10-08T22:01:03.836Z" }, + { url = "https://files.pythonhosted.org/packages/70/8c/f48ac899f7b3ca7eb13af73bacbc93aec37f9c954df3c08ad96991c8c373/tomli-2.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0a154a9ae14bfcf5d8917a59b51ffd5a3ac1fd149b71b47a3a104ca4edcfa845", size = 239555, upload-time = "2025-10-08T22:01:04.834Z" }, + { url = "https://files.pythonhosted.org/packages/ba/28/72f8afd73f1d0e7829bfc093f4cb98ce0a40ffc0cc997009ee1ed94ba705/tomli-2.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:74bf8464ff93e413514fefd2be591c3b0b23231a77f901db1eb30d6f712fc42c", size = 245128, upload-time = "2025-10-08T22:01:05.84Z" }, + { url = "https://files.pythonhosted.org/packages/b6/eb/a7679c8ac85208706d27436e8d421dfa39d4c914dcf5fa8083a9305f58d9/tomli-2.3.0-cp311-cp311-win32.whl", hash = "sha256:00b5f5d95bbfc7d12f91ad8c593a1659b6387b43f054104cda404be6bda62456", size = 96445, upload-time = "2025-10-08T22:01:06.896Z" }, + { url = "https://files.pythonhosted.org/packages/0a/fe/3d3420c4cb1ad9cb462fb52967080575f15898da97e21cb6f1361d505383/tomli-2.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:4dc4ce8483a5d429ab602f111a93a6ab1ed425eae3122032db7e9acf449451be", size = 107165, upload-time = "2025-10-08T22:01:08.107Z" }, + { url = "https://files.pythonhosted.org/packages/ff/b7/40f36368fcabc518bb11c8f06379a0fd631985046c038aca08c6d6a43c6e/tomli-2.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d7d86942e56ded512a594786a5ba0a5e521d02529b3826e7761a05138341a2ac", size = 154891, upload-time = "2025-10-08T22:01:09.082Z" }, + { url = "https://files.pythonhosted.org/packages/f9/3f/d9dd692199e3b3aab2e4e4dd948abd0f790d9ded8cd10cbaae276a898434/tomli-2.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:73ee0b47d4dad1c5e996e3cd33b8a76a50167ae5f96a2607cbe8cc773506ab22", size = 148796, upload-time = "2025-10-08T22:01:10.266Z" }, + { url = "https://files.pythonhosted.org/packages/60/83/59bff4996c2cf9f9387a0f5a3394629c7efa5ef16142076a23a90f1955fa/tomli-2.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:792262b94d5d0a466afb5bc63c7daa9d75520110971ee269152083270998316f", size = 242121, upload-time = "2025-10-08T22:01:11.332Z" }, + { url = "https://files.pythonhosted.org/packages/45/e5/7c5119ff39de8693d6baab6c0b6dcb556d192c165596e9fc231ea1052041/tomli-2.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f195fe57ecceac95a66a75ac24d9d5fbc98ef0962e09b2eddec5d39375aae52", size = 250070, upload-time = "2025-10-08T22:01:12.498Z" }, + { url = "https://files.pythonhosted.org/packages/45/12/ad5126d3a278f27e6701abde51d342aa78d06e27ce2bb596a01f7709a5a2/tomli-2.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e31d432427dcbf4d86958c184b9bfd1e96b5b71f8eb17e6d02531f434fd335b8", size = 245859, upload-time = "2025-10-08T22:01:13.551Z" }, + { url = "https://files.pythonhosted.org/packages/fb/a1/4d6865da6a71c603cfe6ad0e6556c73c76548557a8d658f9e3b142df245f/tomli-2.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b0882799624980785240ab732537fcfc372601015c00f7fc367c55308c186f6", size = 250296, upload-time = "2025-10-08T22:01:14.614Z" }, + { url = "https://files.pythonhosted.org/packages/a0/b7/a7a7042715d55c9ba6e8b196d65d2cb662578b4d8cd17d882d45322b0d78/tomli-2.3.0-cp312-cp312-win32.whl", hash = "sha256:ff72b71b5d10d22ecb084d345fc26f42b5143c5533db5e2eaba7d2d335358876", size = 97124, upload-time = "2025-10-08T22:01:15.629Z" }, + { url = "https://files.pythonhosted.org/packages/06/1e/f22f100db15a68b520664eb3328fb0ae4e90530887928558112c8d1f4515/tomli-2.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:1cb4ed918939151a03f33d4242ccd0aa5f11b3547d0cf30f7c74a408a5b99878", size = 107698, upload-time = "2025-10-08T22:01:16.51Z" }, + { url = "https://files.pythonhosted.org/packages/89/48/06ee6eabe4fdd9ecd48bf488f4ac783844fd777f547b8d1b61c11939974e/tomli-2.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5192f562738228945d7b13d4930baffda67b69425a7f0da96d360b0a3888136b", size = 154819, upload-time = "2025-10-08T22:01:17.964Z" }, + { url = "https://files.pythonhosted.org/packages/f1/01/88793757d54d8937015c75dcdfb673c65471945f6be98e6a0410fba167ed/tomli-2.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:be71c93a63d738597996be9528f4abe628d1adf5e6eb11607bc8fe1a510b5dae", size = 148766, upload-time = "2025-10-08T22:01:18.959Z" }, + { url = "https://files.pythonhosted.org/packages/42/17/5e2c956f0144b812e7e107f94f1cc54af734eb17b5191c0bbfb72de5e93e/tomli-2.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4665508bcbac83a31ff8ab08f424b665200c0e1e645d2bd9ab3d3e557b6185b", size = 240771, upload-time = "2025-10-08T22:01:20.106Z" }, + { url = "https://files.pythonhosted.org/packages/d5/f4/0fbd014909748706c01d16824eadb0307115f9562a15cbb012cd9b3512c5/tomli-2.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4021923f97266babc6ccab9f5068642a0095faa0a51a246a6a02fccbb3514eaf", size = 248586, upload-time = "2025-10-08T22:01:21.164Z" }, + { url = "https://files.pythonhosted.org/packages/30/77/fed85e114bde5e81ecf9bc5da0cc69f2914b38f4708c80ae67d0c10180c5/tomli-2.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4ea38c40145a357d513bffad0ed869f13c1773716cf71ccaa83b0fa0cc4e42f", size = 244792, upload-time = "2025-10-08T22:01:22.417Z" }, + { url = "https://files.pythonhosted.org/packages/55/92/afed3d497f7c186dc71e6ee6d4fcb0acfa5f7d0a1a2878f8beae379ae0cc/tomli-2.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ad805ea85eda330dbad64c7ea7a4556259665bdf9d2672f5dccc740eb9d3ca05", size = 248909, upload-time = "2025-10-08T22:01:23.859Z" }, + { url = "https://files.pythonhosted.org/packages/f8/84/ef50c51b5a9472e7265ce1ffc7f24cd4023d289e109f669bdb1553f6a7c2/tomli-2.3.0-cp313-cp313-win32.whl", hash = "sha256:97d5eec30149fd3294270e889b4234023f2c69747e555a27bd708828353ab606", size = 96946, upload-time = "2025-10-08T22:01:24.893Z" }, + { url = "https://files.pythonhosted.org/packages/b2/b7/718cd1da0884f281f95ccfa3a6cc572d30053cba64603f79d431d3c9b61b/tomli-2.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:0c95ca56fbe89e065c6ead5b593ee64b84a26fca063b5d71a1122bf26e533999", size = 107705, upload-time = "2025-10-08T22:01:26.153Z" }, + { url = "https://files.pythonhosted.org/packages/19/94/aeafa14a52e16163008060506fcb6aa1949d13548d13752171a755c65611/tomli-2.3.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:cebc6fe843e0733ee827a282aca4999b596241195f43b4cc371d64fc6639da9e", size = 154244, upload-time = "2025-10-08T22:01:27.06Z" }, + { url = "https://files.pythonhosted.org/packages/db/e4/1e58409aa78eefa47ccd19779fc6f36787edbe7d4cd330eeeedb33a4515b/tomli-2.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4c2ef0244c75aba9355561272009d934953817c49f47d768070c3c94355c2aa3", size = 148637, upload-time = "2025-10-08T22:01:28.059Z" }, + { url = "https://files.pythonhosted.org/packages/26/b6/d1eccb62f665e44359226811064596dd6a366ea1f985839c566cd61525ae/tomli-2.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c22a8bf253bacc0cf11f35ad9808b6cb75ada2631c2d97c971122583b129afbc", size = 241925, upload-time = "2025-10-08T22:01:29.066Z" }, + { url = "https://files.pythonhosted.org/packages/70/91/7cdab9a03e6d3d2bb11beae108da5bdc1c34bdeb06e21163482544ddcc90/tomli-2.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0eea8cc5c5e9f89c9b90c4896a8deefc74f518db5927d0e0e8d4a80953d774d0", size = 249045, upload-time = "2025-10-08T22:01:31.98Z" }, + { url = "https://files.pythonhosted.org/packages/15/1b/8c26874ed1f6e4f1fcfeb868db8a794cbe9f227299402db58cfcc858766c/tomli-2.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b74a0e59ec5d15127acdabd75ea17726ac4c5178ae51b85bfe39c4f8a278e879", size = 245835, upload-time = "2025-10-08T22:01:32.989Z" }, + { url = "https://files.pythonhosted.org/packages/fd/42/8e3c6a9a4b1a1360c1a2a39f0b972cef2cc9ebd56025168c4137192a9321/tomli-2.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b5870b50c9db823c595983571d1296a6ff3e1b88f734a4c8f6fc6188397de005", size = 253109, upload-time = "2025-10-08T22:01:34.052Z" }, + { url = "https://files.pythonhosted.org/packages/22/0c/b4da635000a71b5f80130937eeac12e686eefb376b8dee113b4a582bba42/tomli-2.3.0-cp314-cp314-win32.whl", hash = "sha256:feb0dacc61170ed7ab602d3d972a58f14ee3ee60494292d384649a3dc38ef463", size = 97930, upload-time = "2025-10-08T22:01:35.082Z" }, + { url = "https://files.pythonhosted.org/packages/b9/74/cb1abc870a418ae99cd5c9547d6bce30701a954e0e721821df483ef7223c/tomli-2.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:b273fcbd7fc64dc3600c098e39136522650c49bca95df2d11cf3b626422392c8", size = 107964, upload-time = "2025-10-08T22:01:36.057Z" }, + { url = "https://files.pythonhosted.org/packages/54/78/5c46fff6432a712af9f792944f4fcd7067d8823157949f4e40c56b8b3c83/tomli-2.3.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:940d56ee0410fa17ee1f12b817b37a4d4e4dc4d27340863cc67236c74f582e77", size = 163065, upload-time = "2025-10-08T22:01:37.27Z" }, + { url = "https://files.pythonhosted.org/packages/39/67/f85d9bd23182f45eca8939cd2bc7050e1f90c41f4a2ecbbd5963a1d1c486/tomli-2.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f85209946d1fe94416debbb88d00eb92ce9cd5266775424ff81bc959e001acaf", size = 159088, upload-time = "2025-10-08T22:01:38.235Z" }, + { url = "https://files.pythonhosted.org/packages/26/5a/4b546a0405b9cc0659b399f12b6adb750757baf04250b148d3c5059fc4eb/tomli-2.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a56212bdcce682e56b0aaf79e869ba5d15a6163f88d5451cbde388d48b13f530", size = 268193, upload-time = "2025-10-08T22:01:39.712Z" }, + { url = "https://files.pythonhosted.org/packages/42/4f/2c12a72ae22cf7b59a7fe75b3465b7aba40ea9145d026ba41cb382075b0e/tomli-2.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c5f3ffd1e098dfc032d4d3af5c0ac64f6d286d98bc148698356847b80fa4de1b", size = 275488, upload-time = "2025-10-08T22:01:40.773Z" }, + { url = "https://files.pythonhosted.org/packages/92/04/a038d65dbe160c3aa5a624e93ad98111090f6804027d474ba9c37c8ae186/tomli-2.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5e01decd096b1530d97d5d85cb4dff4af2d8347bd35686654a004f8dea20fc67", size = 272669, upload-time = "2025-10-08T22:01:41.824Z" }, + { url = "https://files.pythonhosted.org/packages/be/2f/8b7c60a9d1612a7cbc39ffcca4f21a73bf368a80fc25bccf8253e2563267/tomli-2.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8a35dd0e643bb2610f156cca8db95d213a90015c11fee76c946aa62b7ae7e02f", size = 279709, upload-time = "2025-10-08T22:01:43.177Z" }, + { url = "https://files.pythonhosted.org/packages/7e/46/cc36c679f09f27ded940281c38607716c86cf8ba4a518d524e349c8b4874/tomli-2.3.0-cp314-cp314t-win32.whl", hash = "sha256:a1f7f282fe248311650081faafa5f4732bdbfef5d45fe3f2e702fbc6f2d496e0", size = 107563, upload-time = "2025-10-08T22:01:44.233Z" }, + { url = "https://files.pythonhosted.org/packages/84/ff/426ca8683cf7b753614480484f6437f568fd2fda2edbdf57a2d3d8b27a0b/tomli-2.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:70a251f8d4ba2d9ac2542eecf008b3c8a9fc5c3f9f02c56a9d7952612be2fdba", size = 119756, upload-time = "2025-10-08T22:01:45.234Z" }, + { url = "https://files.pythonhosted.org/packages/77/b8/0135fadc89e73be292b473cb820b4f5a08197779206b33191e801feeae40/tomli-2.3.0-py3-none-any.whl", hash = "sha256:e95b1af3c5b07d9e643909b5abbec77cd9f1217e6d0bca72b0234736b9fb1f1b", size = 14408, upload-time = "2025-10-08T22:01:46.04Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "extra == 'group-9-stagehand-pydantic-v2' or extra != 'group-9-stagehand-pydantic-v1'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, +] + +[[package]] +name = "yarl" +version = "1.22.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "multidict" }, + { name = "propcache" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/57/63/0c6ebca57330cd313f6102b16dd57ffaf3ec4c83403dcb45dbd15c6f3ea1/yarl-1.22.0.tar.gz", hash = "sha256:bebf8557577d4401ba8bd9ff33906f1376c877aa78d1fe216ad01b4d6745af71", size = 187169, upload-time = "2025-10-06T14:12:55.963Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/43/a2204825342f37c337f5edb6637040fa14e365b2fcc2346960201d457579/yarl-1.22.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:c7bd6683587567e5a49ee6e336e0612bec8329be1b7d4c8af5687dcdeb67ee1e", size = 140517, upload-time = "2025-10-06T14:08:42.494Z" }, + { url = "https://files.pythonhosted.org/packages/44/6f/674f3e6f02266428c56f704cd2501c22f78e8b2eeb23f153117cc86fb28a/yarl-1.22.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5cdac20da754f3a723cceea5b3448e1a2074866406adeb4ef35b469d089adb8f", size = 93495, upload-time = "2025-10-06T14:08:46.2Z" }, + { url = "https://files.pythonhosted.org/packages/b8/12/5b274d8a0f30c07b91b2f02cba69152600b47830fcfb465c108880fcee9c/yarl-1.22.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:07a524d84df0c10f41e3ee918846e1974aba4ec017f990dc735aad487a0bdfdf", size = 94400, upload-time = "2025-10-06T14:08:47.855Z" }, + { url = "https://files.pythonhosted.org/packages/e2/7f/df1b6949b1fa1aa9ff6de6e2631876ad4b73c4437822026e85d8acb56bb1/yarl-1.22.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e1b329cb8146d7b736677a2440e422eadd775d1806a81db2d4cded80a48efc1a", size = 347545, upload-time = "2025-10-06T14:08:49.683Z" }, + { url = "https://files.pythonhosted.org/packages/84/09/f92ed93bd6cd77872ab6c3462df45ca45cd058d8f1d0c9b4f54c1704429f/yarl-1.22.0-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:75976c6945d85dbb9ee6308cd7ff7b1fb9409380c82d6119bd778d8fcfe2931c", size = 319598, upload-time = "2025-10-06T14:08:51.215Z" }, + { url = "https://files.pythonhosted.org/packages/c3/97/ac3f3feae7d522cf7ccec3d340bb0b2b61c56cb9767923df62a135092c6b/yarl-1.22.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:80ddf7a5f8c86cb3eb4bc9028b07bbbf1f08a96c5c0bc1244be5e8fefcb94147", size = 363893, upload-time = "2025-10-06T14:08:53.144Z" }, + { url = "https://files.pythonhosted.org/packages/06/49/f3219097403b9c84a4d079b1d7bda62dd9b86d0d6e4428c02d46ab2c77fc/yarl-1.22.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d332fc2e3c94dad927f2112395772a4e4fedbcf8f80efc21ed7cdfae4d574fdb", size = 371240, upload-time = "2025-10-06T14:08:55.036Z" }, + { url = "https://files.pythonhosted.org/packages/35/9f/06b765d45c0e44e8ecf0fe15c9eacbbde342bb5b7561c46944f107bfb6c3/yarl-1.22.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0cf71bf877efeac18b38d3930594c0948c82b64547c1cf420ba48722fe5509f6", size = 346965, upload-time = "2025-10-06T14:08:56.722Z" }, + { url = "https://files.pythonhosted.org/packages/c5/69/599e7cea8d0fcb1694323b0db0dda317fa3162f7b90166faddecf532166f/yarl-1.22.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:663e1cadaddae26be034a6ab6072449a8426ddb03d500f43daf952b74553bba0", size = 342026, upload-time = "2025-10-06T14:08:58.563Z" }, + { url = "https://files.pythonhosted.org/packages/95/6f/9dfd12c8bc90fea9eab39832ee32ea48f8e53d1256252a77b710c065c89f/yarl-1.22.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:6dcbb0829c671f305be48a7227918cfcd11276c2d637a8033a99a02b67bf9eda", size = 335637, upload-time = "2025-10-06T14:09:00.506Z" }, + { url = "https://files.pythonhosted.org/packages/57/2e/34c5b4eb9b07e16e873db5b182c71e5f06f9b5af388cdaa97736d79dd9a6/yarl-1.22.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:f0d97c18dfd9a9af4490631905a3f131a8e4c9e80a39353919e2cfed8f00aedc", size = 359082, upload-time = "2025-10-06T14:09:01.936Z" }, + { url = "https://files.pythonhosted.org/packages/31/71/fa7e10fb772d273aa1f096ecb8ab8594117822f683bab7d2c5a89914c92a/yarl-1.22.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:437840083abe022c978470b942ff832c3940b2ad3734d424b7eaffcd07f76737", size = 357811, upload-time = "2025-10-06T14:09:03.445Z" }, + { url = "https://files.pythonhosted.org/packages/26/da/11374c04e8e1184a6a03cf9c8f5688d3e5cec83ed6f31ad3481b3207f709/yarl-1.22.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:a899cbd98dce6f5d8de1aad31cb712ec0a530abc0a86bd6edaa47c1090138467", size = 351223, upload-time = "2025-10-06T14:09:05.401Z" }, + { url = "https://files.pythonhosted.org/packages/82/8f/e2d01f161b0c034a30410e375e191a5d27608c1f8693bab1a08b089ca096/yarl-1.22.0-cp310-cp310-win32.whl", hash = "sha256:595697f68bd1f0c1c159fcb97b661fc9c3f5db46498043555d04805430e79bea", size = 82118, upload-time = "2025-10-06T14:09:11.148Z" }, + { url = "https://files.pythonhosted.org/packages/62/46/94c76196642dbeae634c7a61ba3da88cd77bed875bf6e4a8bed037505aa6/yarl-1.22.0-cp310-cp310-win_amd64.whl", hash = "sha256:cb95a9b1adaa48e41815a55ae740cfda005758104049a640a398120bf02515ca", size = 86852, upload-time = "2025-10-06T14:09:12.958Z" }, + { url = "https://files.pythonhosted.org/packages/af/af/7df4f179d3b1a6dcb9a4bd2ffbc67642746fcafdb62580e66876ce83fff4/yarl-1.22.0-cp310-cp310-win_arm64.whl", hash = "sha256:b85b982afde6df99ecc996990d4ad7ccbdbb70e2a4ba4de0aecde5922ba98a0b", size = 82012, upload-time = "2025-10-06T14:09:14.664Z" }, + { url = "https://files.pythonhosted.org/packages/4d/27/5ab13fc84c76a0250afd3d26d5936349a35be56ce5785447d6c423b26d92/yarl-1.22.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:1ab72135b1f2db3fed3997d7e7dc1b80573c67138023852b6efb336a5eae6511", size = 141607, upload-time = "2025-10-06T14:09:16.298Z" }, + { url = "https://files.pythonhosted.org/packages/6a/a1/d065d51d02dc02ce81501d476b9ed2229d9a990818332242a882d5d60340/yarl-1.22.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:669930400e375570189492dc8d8341301578e8493aec04aebc20d4717f899dd6", size = 94027, upload-time = "2025-10-06T14:09:17.786Z" }, + { url = "https://files.pythonhosted.org/packages/c1/da/8da9f6a53f67b5106ffe902c6fa0164e10398d4e150d85838b82f424072a/yarl-1.22.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:792a2af6d58177ef7c19cbf0097aba92ca1b9cb3ffdd9c7470e156c8f9b5e028", size = 94963, upload-time = "2025-10-06T14:09:19.662Z" }, + { url = "https://files.pythonhosted.org/packages/68/fe/2c1f674960c376e29cb0bec1249b117d11738db92a6ccc4a530b972648db/yarl-1.22.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3ea66b1c11c9150f1372f69afb6b8116f2dd7286f38e14ea71a44eee9ec51b9d", size = 368406, upload-time = "2025-10-06T14:09:21.402Z" }, + { url = "https://files.pythonhosted.org/packages/95/26/812a540e1c3c6418fec60e9bbd38e871eaba9545e94fa5eff8f4a8e28e1e/yarl-1.22.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3e2daa88dc91870215961e96a039ec73e4937da13cf77ce17f9cad0c18df3503", size = 336581, upload-time = "2025-10-06T14:09:22.98Z" }, + { url = "https://files.pythonhosted.org/packages/0b/f5/5777b19e26fdf98563985e481f8be3d8a39f8734147a6ebf459d0dab5a6b/yarl-1.22.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ba440ae430c00eee41509353628600212112cd5018d5def7e9b05ea7ac34eb65", size = 388924, upload-time = "2025-10-06T14:09:24.655Z" }, + { url = "https://files.pythonhosted.org/packages/86/08/24bd2477bd59c0bbd994fe1d93b126e0472e4e3df5a96a277b0a55309e89/yarl-1.22.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e6438cc8f23a9c1478633d216b16104a586b9761db62bfacb6425bac0a36679e", size = 392890, upload-time = "2025-10-06T14:09:26.617Z" }, + { url = "https://files.pythonhosted.org/packages/46/00/71b90ed48e895667ecfb1eaab27c1523ee2fa217433ed77a73b13205ca4b/yarl-1.22.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c52a6e78aef5cf47a98ef8e934755abf53953379b7d53e68b15ff4420e6683d", size = 365819, upload-time = "2025-10-06T14:09:28.544Z" }, + { url = "https://files.pythonhosted.org/packages/30/2d/f715501cae832651d3282387c6a9236cd26bd00d0ff1e404b3dc52447884/yarl-1.22.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3b06bcadaac49c70f4c88af4ffcfbe3dc155aab3163e75777818092478bcbbe7", size = 363601, upload-time = "2025-10-06T14:09:30.568Z" }, + { url = "https://files.pythonhosted.org/packages/f8/f9/a678c992d78e394e7126ee0b0e4e71bd2775e4334d00a9278c06a6cce96a/yarl-1.22.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:6944b2dc72c4d7f7052683487e3677456050ff77fcf5e6204e98caf785ad1967", size = 358072, upload-time = "2025-10-06T14:09:32.528Z" }, + { url = "https://files.pythonhosted.org/packages/2c/d1/b49454411a60edb6fefdcad4f8e6dbba7d8019e3a508a1c5836cba6d0781/yarl-1.22.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:d5372ca1df0f91a86b047d1277c2aaf1edb32d78bbcefffc81b40ffd18f027ed", size = 385311, upload-time = "2025-10-06T14:09:34.634Z" }, + { url = "https://files.pythonhosted.org/packages/87/e5/40d7a94debb8448c7771a916d1861d6609dddf7958dc381117e7ba36d9e8/yarl-1.22.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:51af598701f5299012b8416486b40fceef8c26fc87dc6d7d1f6fc30609ea0aa6", size = 381094, upload-time = "2025-10-06T14:09:36.268Z" }, + { url = "https://files.pythonhosted.org/packages/35/d8/611cc282502381ad855448643e1ad0538957fc82ae83dfe7762c14069e14/yarl-1.22.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b266bd01fedeffeeac01a79ae181719ff848a5a13ce10075adbefc8f1daee70e", size = 370944, upload-time = "2025-10-06T14:09:37.872Z" }, + { url = "https://files.pythonhosted.org/packages/2d/df/fadd00fb1c90e1a5a8bd731fa3d3de2e165e5a3666a095b04e31b04d9cb6/yarl-1.22.0-cp311-cp311-win32.whl", hash = "sha256:a9b1ba5610a4e20f655258d5a1fdc7ebe3d837bb0e45b581398b99eb98b1f5ca", size = 81804, upload-time = "2025-10-06T14:09:39.359Z" }, + { url = "https://files.pythonhosted.org/packages/b5/f7/149bb6f45f267cb5c074ac40c01c6b3ea6d8a620d34b337f6321928a1b4d/yarl-1.22.0-cp311-cp311-win_amd64.whl", hash = "sha256:078278b9b0b11568937d9509b589ee83ef98ed6d561dfe2020e24a9fd08eaa2b", size = 86858, upload-time = "2025-10-06T14:09:41.068Z" }, + { url = "https://files.pythonhosted.org/packages/2b/13/88b78b93ad3f2f0b78e13bfaaa24d11cbc746e93fe76d8c06bf139615646/yarl-1.22.0-cp311-cp311-win_arm64.whl", hash = "sha256:b6a6f620cfe13ccec221fa312139135166e47ae169f8253f72a0abc0dae94376", size = 81637, upload-time = "2025-10-06T14:09:42.712Z" }, + { url = "https://files.pythonhosted.org/packages/75/ff/46736024fee3429b80a165a732e38e5d5a238721e634ab41b040d49f8738/yarl-1.22.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e340382d1afa5d32b892b3ff062436d592ec3d692aeea3bef3a5cfe11bbf8c6f", size = 142000, upload-time = "2025-10-06T14:09:44.631Z" }, + { url = "https://files.pythonhosted.org/packages/5a/9a/b312ed670df903145598914770eb12de1bac44599549b3360acc96878df8/yarl-1.22.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f1e09112a2c31ffe8d80be1b0988fa6a18c5d5cad92a9ffbb1c04c91bfe52ad2", size = 94338, upload-time = "2025-10-06T14:09:46.372Z" }, + { url = "https://files.pythonhosted.org/packages/ba/f5/0601483296f09c3c65e303d60c070a5c19fcdbc72daa061e96170785bc7d/yarl-1.22.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:939fe60db294c786f6b7c2d2e121576628468f65453d86b0fe36cb52f987bd74", size = 94909, upload-time = "2025-10-06T14:09:48.648Z" }, + { url = "https://files.pythonhosted.org/packages/60/41/9a1fe0b73dbcefce72e46cf149b0e0a67612d60bfc90fb59c2b2efdfbd86/yarl-1.22.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e1651bf8e0398574646744c1885a41198eba53dc8a9312b954073f845c90a8df", size = 372940, upload-time = "2025-10-06T14:09:50.089Z" }, + { url = "https://files.pythonhosted.org/packages/17/7a/795cb6dfee561961c30b800f0ed616b923a2ec6258b5def2a00bf8231334/yarl-1.22.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b8a0588521a26bf92a57a1705b77b8b59044cdceccac7151bd8d229e66b8dedb", size = 345825, upload-time = "2025-10-06T14:09:52.142Z" }, + { url = "https://files.pythonhosted.org/packages/d7/93/a58f4d596d2be2ae7bab1a5846c4d270b894958845753b2c606d666744d3/yarl-1.22.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:42188e6a615c1a75bcaa6e150c3fe8f3e8680471a6b10150c5f7e83f47cc34d2", size = 386705, upload-time = "2025-10-06T14:09:54.128Z" }, + { url = "https://files.pythonhosted.org/packages/61/92/682279d0e099d0e14d7fd2e176bd04f48de1484f56546a3e1313cd6c8e7c/yarl-1.22.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f6d2cb59377d99718913ad9a151030d6f83ef420a2b8f521d94609ecc106ee82", size = 396518, upload-time = "2025-10-06T14:09:55.762Z" }, + { url = "https://files.pythonhosted.org/packages/db/0f/0d52c98b8a885aeda831224b78f3be7ec2e1aa4a62091f9f9188c3c65b56/yarl-1.22.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50678a3b71c751d58d7908edc96d332af328839eea883bb554a43f539101277a", size = 377267, upload-time = "2025-10-06T14:09:57.958Z" }, + { url = "https://files.pythonhosted.org/packages/22/42/d2685e35908cbeaa6532c1fc73e89e7f2efb5d8a7df3959ea8e37177c5a3/yarl-1.22.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1e8fbaa7cec507aa24ea27a01456e8dd4b6fab829059b69844bd348f2d467124", size = 365797, upload-time = "2025-10-06T14:09:59.527Z" }, + { url = "https://files.pythonhosted.org/packages/a2/83/cf8c7bcc6355631762f7d8bdab920ad09b82efa6b722999dfb05afa6cfac/yarl-1.22.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:433885ab5431bc3d3d4f2f9bd15bfa1614c522b0f1405d62c4f926ccd69d04fa", size = 365535, upload-time = "2025-10-06T14:10:01.139Z" }, + { url = "https://files.pythonhosted.org/packages/25/e1/5302ff9b28f0c59cac913b91fe3f16c59a033887e57ce9ca5d41a3a94737/yarl-1.22.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:b790b39c7e9a4192dc2e201a282109ed2985a1ddbd5ac08dc56d0e121400a8f7", size = 382324, upload-time = "2025-10-06T14:10:02.756Z" }, + { url = "https://files.pythonhosted.org/packages/bf/cd/4617eb60f032f19ae3a688dc990d8f0d89ee0ea378b61cac81ede3e52fae/yarl-1.22.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:31f0b53913220599446872d757257be5898019c85e7971599065bc55065dc99d", size = 383803, upload-time = "2025-10-06T14:10:04.552Z" }, + { url = "https://files.pythonhosted.org/packages/59/65/afc6e62bb506a319ea67b694551dab4a7e6fb7bf604e9bd9f3e11d575fec/yarl-1.22.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a49370e8f711daec68d09b821a34e1167792ee2d24d405cbc2387be4f158b520", size = 374220, upload-time = "2025-10-06T14:10:06.489Z" }, + { url = "https://files.pythonhosted.org/packages/e7/3d/68bf18d50dc674b942daec86a9ba922d3113d8399b0e52b9897530442da2/yarl-1.22.0-cp312-cp312-win32.whl", hash = "sha256:70dfd4f241c04bd9239d53b17f11e6ab672b9f1420364af63e8531198e3f5fe8", size = 81589, upload-time = "2025-10-06T14:10:09.254Z" }, + { url = "https://files.pythonhosted.org/packages/c8/9a/6ad1a9b37c2f72874f93e691b2e7ecb6137fb2b899983125db4204e47575/yarl-1.22.0-cp312-cp312-win_amd64.whl", hash = "sha256:8884d8b332a5e9b88e23f60bb166890009429391864c685e17bd73a9eda9105c", size = 87213, upload-time = "2025-10-06T14:10:11.369Z" }, + { url = "https://files.pythonhosted.org/packages/44/c5/c21b562d1680a77634d748e30c653c3ca918beb35555cff24986fff54598/yarl-1.22.0-cp312-cp312-win_arm64.whl", hash = "sha256:ea70f61a47f3cc93bdf8b2f368ed359ef02a01ca6393916bc8ff877427181e74", size = 81330, upload-time = "2025-10-06T14:10:13.112Z" }, + { url = "https://files.pythonhosted.org/packages/ea/f3/d67de7260456ee105dc1d162d43a019ecad6b91e2f51809d6cddaa56690e/yarl-1.22.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8dee9c25c74997f6a750cd317b8ca63545169c098faee42c84aa5e506c819b53", size = 139980, upload-time = "2025-10-06T14:10:14.601Z" }, + { url = "https://files.pythonhosted.org/packages/01/88/04d98af0b47e0ef42597b9b28863b9060bb515524da0a65d5f4db160b2d5/yarl-1.22.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:01e73b85a5434f89fc4fe27dcda2aff08ddf35e4d47bbbea3bdcd25321af538a", size = 93424, upload-time = "2025-10-06T14:10:16.115Z" }, + { url = "https://files.pythonhosted.org/packages/18/91/3274b215fd8442a03975ce6bee5fe6aa57a8326b29b9d3d56234a1dca244/yarl-1.22.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:22965c2af250d20c873cdbee8ff958fb809940aeb2e74ba5f20aaf6b7ac8c70c", size = 93821, upload-time = "2025-10-06T14:10:17.993Z" }, + { url = "https://files.pythonhosted.org/packages/61/3a/caf4e25036db0f2da4ca22a353dfeb3c9d3c95d2761ebe9b14df8fc16eb0/yarl-1.22.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b4f15793aa49793ec8d1c708ab7f9eded1aa72edc5174cae703651555ed1b601", size = 373243, upload-time = "2025-10-06T14:10:19.44Z" }, + { url = "https://files.pythonhosted.org/packages/6e/9e/51a77ac7516e8e7803b06e01f74e78649c24ee1021eca3d6a739cb6ea49c/yarl-1.22.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5542339dcf2747135c5c85f68680353d5cb9ffd741c0f2e8d832d054d41f35a", size = 342361, upload-time = "2025-10-06T14:10:21.124Z" }, + { url = "https://files.pythonhosted.org/packages/d4/f8/33b92454789dde8407f156c00303e9a891f1f51a0330b0fad7c909f87692/yarl-1.22.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5c401e05ad47a75869c3ab3e35137f8468b846770587e70d71e11de797d113df", size = 387036, upload-time = "2025-10-06T14:10:22.902Z" }, + { url = "https://files.pythonhosted.org/packages/d9/9a/c5db84ea024f76838220280f732970aa4ee154015d7f5c1bfb60a267af6f/yarl-1.22.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:243dda95d901c733f5b59214d28b0120893d91777cb8aa043e6ef059d3cddfe2", size = 397671, upload-time = "2025-10-06T14:10:24.523Z" }, + { url = "https://files.pythonhosted.org/packages/11/c9/cd8538dc2e7727095e0c1d867bad1e40c98f37763e6d995c1939f5fdc7b1/yarl-1.22.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bec03d0d388060058f5d291a813f21c011041938a441c593374da6077fe21b1b", size = 377059, upload-time = "2025-10-06T14:10:26.406Z" }, + { url = "https://files.pythonhosted.org/packages/a1/b9/ab437b261702ced75122ed78a876a6dec0a1b0f5e17a4ac7a9a2482d8abe/yarl-1.22.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b0748275abb8c1e1e09301ee3cf90c8a99678a4e92e4373705f2a2570d581273", size = 365356, upload-time = "2025-10-06T14:10:28.461Z" }, + { url = "https://files.pythonhosted.org/packages/b2/9d/8e1ae6d1d008a9567877b08f0ce4077a29974c04c062dabdb923ed98e6fe/yarl-1.22.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:47fdb18187e2a4e18fda2c25c05d8251a9e4a521edaed757fef033e7d8498d9a", size = 361331, upload-time = "2025-10-06T14:10:30.541Z" }, + { url = "https://files.pythonhosted.org/packages/ca/5a/09b7be3905962f145b73beb468cdd53db8aa171cf18c80400a54c5b82846/yarl-1.22.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c7044802eec4524fde550afc28edda0dd5784c4c45f0be151a2d3ba017daca7d", size = 382590, upload-time = "2025-10-06T14:10:33.352Z" }, + { url = "https://files.pythonhosted.org/packages/aa/7f/59ec509abf90eda5048b0bc3e2d7b5099dffdb3e6b127019895ab9d5ef44/yarl-1.22.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:139718f35149ff544caba20fce6e8a2f71f1e39b92c700d8438a0b1d2a631a02", size = 385316, upload-time = "2025-10-06T14:10:35.034Z" }, + { url = "https://files.pythonhosted.org/packages/e5/84/891158426bc8036bfdfd862fabd0e0fa25df4176ec793e447f4b85cf1be4/yarl-1.22.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e1b51bebd221006d3d2f95fbe124b22b247136647ae5dcc8c7acafba66e5ee67", size = 374431, upload-time = "2025-10-06T14:10:37.76Z" }, + { url = "https://files.pythonhosted.org/packages/bb/49/03da1580665baa8bef5e8ed34c6df2c2aca0a2f28bf397ed238cc1bbc6f2/yarl-1.22.0-cp313-cp313-win32.whl", hash = "sha256:d3e32536234a95f513bd374e93d717cf6b2231a791758de6c509e3653f234c95", size = 81555, upload-time = "2025-10-06T14:10:39.649Z" }, + { url = "https://files.pythonhosted.org/packages/9a/ee/450914ae11b419eadd067c6183ae08381cfdfcb9798b90b2b713bbebddda/yarl-1.22.0-cp313-cp313-win_amd64.whl", hash = "sha256:47743b82b76d89a1d20b83e60d5c20314cbd5ba2befc9cda8f28300c4a08ed4d", size = 86965, upload-time = "2025-10-06T14:10:41.313Z" }, + { url = "https://files.pythonhosted.org/packages/98/4d/264a01eae03b6cf629ad69bae94e3b0e5344741e929073678e84bf7a3e3b/yarl-1.22.0-cp313-cp313-win_arm64.whl", hash = "sha256:5d0fcda9608875f7d052eff120c7a5da474a6796fe4d83e152e0e4d42f6d1a9b", size = 81205, upload-time = "2025-10-06T14:10:43.167Z" }, + { url = "https://files.pythonhosted.org/packages/88/fc/6908f062a2f77b5f9f6d69cecb1747260831ff206adcbc5b510aff88df91/yarl-1.22.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:719ae08b6972befcba4310e49edb1161a88cdd331e3a694b84466bd938a6ab10", size = 146209, upload-time = "2025-10-06T14:10:44.643Z" }, + { url = "https://files.pythonhosted.org/packages/65/47/76594ae8eab26210b4867be6f49129861ad33da1f1ebdf7051e98492bf62/yarl-1.22.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:47d8a5c446df1c4db9d21b49619ffdba90e77c89ec6e283f453856c74b50b9e3", size = 95966, upload-time = "2025-10-06T14:10:46.554Z" }, + { url = "https://files.pythonhosted.org/packages/ab/ce/05e9828a49271ba6b5b038b15b3934e996980dd78abdfeb52a04cfb9467e/yarl-1.22.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:cfebc0ac8333520d2d0423cbbe43ae43c8838862ddb898f5ca68565e395516e9", size = 97312, upload-time = "2025-10-06T14:10:48.007Z" }, + { url = "https://files.pythonhosted.org/packages/d1/c5/7dffad5e4f2265b29c9d7ec869c369e4223166e4f9206fc2243ee9eea727/yarl-1.22.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4398557cbf484207df000309235979c79c4356518fd5c99158c7d38203c4da4f", size = 361967, upload-time = "2025-10-06T14:10:49.997Z" }, + { url = "https://files.pythonhosted.org/packages/50/b2/375b933c93a54bff7fc041e1a6ad2c0f6f733ffb0c6e642ce56ee3b39970/yarl-1.22.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2ca6fd72a8cd803be290d42f2dec5cdcd5299eeb93c2d929bf060ad9efaf5de0", size = 323949, upload-time = "2025-10-06T14:10:52.004Z" }, + { url = "https://files.pythonhosted.org/packages/66/50/bfc2a29a1d78644c5a7220ce2f304f38248dc94124a326794e677634b6cf/yarl-1.22.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ca1f59c4e1ab6e72f0a23c13fca5430f889634166be85dbf1013683e49e3278e", size = 361818, upload-time = "2025-10-06T14:10:54.078Z" }, + { url = "https://files.pythonhosted.org/packages/46/96/f3941a46af7d5d0f0498f86d71275696800ddcdd20426298e572b19b91ff/yarl-1.22.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6c5010a52015e7c70f86eb967db0f37f3c8bd503a695a49f8d45700144667708", size = 372626, upload-time = "2025-10-06T14:10:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/c1/42/8b27c83bb875cd89448e42cd627e0fb971fa1675c9ec546393d18826cb50/yarl-1.22.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d7672ecf7557476642c88497c2f8d8542f8e36596e928e9bcba0e42e1e7d71f", size = 341129, upload-time = "2025-10-06T14:10:57.985Z" }, + { url = "https://files.pythonhosted.org/packages/49/36/99ca3122201b382a3cf7cc937b95235b0ac944f7e9f2d5331d50821ed352/yarl-1.22.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:3b7c88eeef021579d600e50363e0b6ee4f7f6f728cd3486b9d0f3ee7b946398d", size = 346776, upload-time = "2025-10-06T14:10:59.633Z" }, + { url = "https://files.pythonhosted.org/packages/85/b4/47328bf996acd01a4c16ef9dcd2f59c969f495073616586f78cd5f2efb99/yarl-1.22.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:f4afb5c34f2c6fecdcc182dfcfc6af6cccf1aa923eed4d6a12e9d96904e1a0d8", size = 334879, upload-time = "2025-10-06T14:11:01.454Z" }, + { url = "https://files.pythonhosted.org/packages/c2/ad/b77d7b3f14a4283bffb8e92c6026496f6de49751c2f97d4352242bba3990/yarl-1.22.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:59c189e3e99a59cf8d83cbb31d4db02d66cda5a1a4374e8a012b51255341abf5", size = 350996, upload-time = "2025-10-06T14:11:03.452Z" }, + { url = "https://files.pythonhosted.org/packages/81/c8/06e1d69295792ba54d556f06686cbd6a7ce39c22307100e3fb4a2c0b0a1d/yarl-1.22.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:5a3bf7f62a289fa90f1990422dc8dff5a458469ea71d1624585ec3a4c8d6960f", size = 356047, upload-time = "2025-10-06T14:11:05.115Z" }, + { url = "https://files.pythonhosted.org/packages/4b/b8/4c0e9e9f597074b208d18cef227d83aac36184bfbc6eab204ea55783dbc5/yarl-1.22.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:de6b9a04c606978fdfe72666fa216ffcf2d1a9f6a381058d4378f8d7b1e5de62", size = 342947, upload-time = "2025-10-06T14:11:08.137Z" }, + { url = "https://files.pythonhosted.org/packages/e0/e5/11f140a58bf4c6ad7aca69a892bff0ee638c31bea4206748fc0df4ebcb3a/yarl-1.22.0-cp313-cp313t-win32.whl", hash = "sha256:1834bb90991cc2999f10f97f5f01317f99b143284766d197e43cd5b45eb18d03", size = 86943, upload-time = "2025-10-06T14:11:10.284Z" }, + { url = "https://files.pythonhosted.org/packages/31/74/8b74bae38ed7fe6793d0c15a0c8207bbb819cf287788459e5ed230996cdd/yarl-1.22.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ff86011bd159a9d2dfc89c34cfd8aff12875980e3bd6a39ff097887520e60249", size = 93715, upload-time = "2025-10-06T14:11:11.739Z" }, + { url = "https://files.pythonhosted.org/packages/69/66/991858aa4b5892d57aef7ee1ba6b4d01ec3b7eb3060795d34090a3ca3278/yarl-1.22.0-cp313-cp313t-win_arm64.whl", hash = "sha256:7861058d0582b847bc4e3a4a4c46828a410bca738673f35a29ba3ca5db0b473b", size = 83857, upload-time = "2025-10-06T14:11:13.586Z" }, + { url = "https://files.pythonhosted.org/packages/46/b3/e20ef504049f1a1c54a814b4b9bed96d1ac0e0610c3b4da178f87209db05/yarl-1.22.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:34b36c2c57124530884d89d50ed2c1478697ad7473efd59cfd479945c95650e4", size = 140520, upload-time = "2025-10-06T14:11:15.465Z" }, + { url = "https://files.pythonhosted.org/packages/e4/04/3532d990fdbab02e5ede063676b5c4260e7f3abea2151099c2aa745acc4c/yarl-1.22.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:0dd9a702591ca2e543631c2a017e4a547e38a5c0f29eece37d9097e04a7ac683", size = 93504, upload-time = "2025-10-06T14:11:17.106Z" }, + { url = "https://files.pythonhosted.org/packages/11/63/ff458113c5c2dac9a9719ac68ee7c947cb621432bcf28c9972b1c0e83938/yarl-1.22.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:594fcab1032e2d2cc3321bb2e51271e7cd2b516c7d9aee780ece81b07ff8244b", size = 94282, upload-time = "2025-10-06T14:11:19.064Z" }, + { url = "https://files.pythonhosted.org/packages/a7/bc/315a56aca762d44a6aaaf7ad253f04d996cb6b27bad34410f82d76ea8038/yarl-1.22.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f3d7a87a78d46a2e3d5b72587ac14b4c16952dd0887dbb051451eceac774411e", size = 372080, upload-time = "2025-10-06T14:11:20.996Z" }, + { url = "https://files.pythonhosted.org/packages/3f/3f/08e9b826ec2e099ea6e7c69a61272f4f6da62cb5b1b63590bb80ca2e4a40/yarl-1.22.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:852863707010316c973162e703bddabec35e8757e67fcb8ad58829de1ebc8590", size = 338696, upload-time = "2025-10-06T14:11:22.847Z" }, + { url = "https://files.pythonhosted.org/packages/e3/9f/90360108e3b32bd76789088e99538febfea24a102380ae73827f62073543/yarl-1.22.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:131a085a53bfe839a477c0845acf21efc77457ba2bcf5899618136d64f3303a2", size = 387121, upload-time = "2025-10-06T14:11:24.889Z" }, + { url = "https://files.pythonhosted.org/packages/98/92/ab8d4657bd5b46a38094cfaea498f18bb70ce6b63508fd7e909bd1f93066/yarl-1.22.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:078a8aefd263f4d4f923a9677b942b445a2be970ca24548a8102689a3a8ab8da", size = 394080, upload-time = "2025-10-06T14:11:27.307Z" }, + { url = "https://files.pythonhosted.org/packages/f5/e7/d8c5a7752fef68205296201f8ec2bf718f5c805a7a7e9880576c67600658/yarl-1.22.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bca03b91c323036913993ff5c738d0842fc9c60c4648e5c8d98331526df89784", size = 372661, upload-time = "2025-10-06T14:11:29.387Z" }, + { url = "https://files.pythonhosted.org/packages/b6/2e/f4d26183c8db0bb82d491b072f3127fb8c381a6206a3a56332714b79b751/yarl-1.22.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:68986a61557d37bb90d3051a45b91fa3d5c516d177dfc6dd6f2f436a07ff2b6b", size = 364645, upload-time = "2025-10-06T14:11:31.423Z" }, + { url = "https://files.pythonhosted.org/packages/80/7c/428e5812e6b87cd00ee8e898328a62c95825bf37c7fa87f0b6bb2ad31304/yarl-1.22.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:4792b262d585ff0dff6bcb787f8492e40698443ec982a3568c2096433660c694", size = 355361, upload-time = "2025-10-06T14:11:33.055Z" }, + { url = "https://files.pythonhosted.org/packages/ec/2a/249405fd26776f8b13c067378ef4d7dd49c9098d1b6457cdd152a99e96a9/yarl-1.22.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:ebd4549b108d732dba1d4ace67614b9545b21ece30937a63a65dd34efa19732d", size = 381451, upload-time = "2025-10-06T14:11:35.136Z" }, + { url = "https://files.pythonhosted.org/packages/67/a8/fb6b1adbe98cf1e2dd9fad71003d3a63a1bc22459c6e15f5714eb9323b93/yarl-1.22.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f87ac53513d22240c7d59203f25cc3beac1e574c6cd681bbfd321987b69f95fd", size = 383814, upload-time = "2025-10-06T14:11:37.094Z" }, + { url = "https://files.pythonhosted.org/packages/d9/f9/3aa2c0e480fb73e872ae2814c43bc1e734740bb0d54e8cb2a95925f98131/yarl-1.22.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:22b029f2881599e2f1b06f8f1db2ee63bd309e2293ba2d566e008ba12778b8da", size = 370799, upload-time = "2025-10-06T14:11:38.83Z" }, + { url = "https://files.pythonhosted.org/packages/50/3c/af9dba3b8b5eeb302f36f16f92791f3ea62e3f47763406abf6d5a4a3333b/yarl-1.22.0-cp314-cp314-win32.whl", hash = "sha256:6a635ea45ba4ea8238463b4f7d0e721bad669f80878b7bfd1f89266e2ae63da2", size = 82990, upload-time = "2025-10-06T14:11:40.624Z" }, + { url = "https://files.pythonhosted.org/packages/ac/30/ac3a0c5bdc1d6efd1b41fa24d4897a4329b3b1e98de9449679dd327af4f0/yarl-1.22.0-cp314-cp314-win_amd64.whl", hash = "sha256:0d6e6885777af0f110b0e5d7e5dda8b704efed3894da26220b7f3d887b839a79", size = 88292, upload-time = "2025-10-06T14:11:42.578Z" }, + { url = "https://files.pythonhosted.org/packages/df/0a/227ab4ff5b998a1b7410abc7b46c9b7a26b0ca9e86c34ba4b8d8bc7c63d5/yarl-1.22.0-cp314-cp314-win_arm64.whl", hash = "sha256:8218f4e98d3c10d683584cb40f0424f4b9fd6e95610232dd75e13743b070ee33", size = 82888, upload-time = "2025-10-06T14:11:44.863Z" }, + { url = "https://files.pythonhosted.org/packages/06/5e/a15eb13db90abd87dfbefb9760c0f3f257ac42a5cac7e75dbc23bed97a9f/yarl-1.22.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:45c2842ff0e0d1b35a6bf1cd6c690939dacb617a70827f715232b2e0494d55d1", size = 146223, upload-time = "2025-10-06T14:11:46.796Z" }, + { url = "https://files.pythonhosted.org/packages/18/82/9665c61910d4d84f41a5bf6837597c89e665fa88aa4941080704645932a9/yarl-1.22.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:d947071e6ebcf2e2bee8fce76e10faca8f7a14808ca36a910263acaacef08eca", size = 95981, upload-time = "2025-10-06T14:11:48.845Z" }, + { url = "https://files.pythonhosted.org/packages/5d/9a/2f65743589809af4d0a6d3aa749343c4b5f4c380cc24a8e94a3c6625a808/yarl-1.22.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:334b8721303e61b00019474cc103bdac3d7b1f65e91f0bfedeec2d56dfe74b53", size = 97303, upload-time = "2025-10-06T14:11:50.897Z" }, + { url = "https://files.pythonhosted.org/packages/b0/ab/5b13d3e157505c43c3b43b5a776cbf7b24a02bc4cccc40314771197e3508/yarl-1.22.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1e7ce67c34138a058fd092f67d07a72b8e31ff0c9236e751957465a24b28910c", size = 361820, upload-time = "2025-10-06T14:11:52.549Z" }, + { url = "https://files.pythonhosted.org/packages/fb/76/242a5ef4677615cf95330cfc1b4610e78184400699bdda0acb897ef5e49a/yarl-1.22.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d77e1b2c6d04711478cb1c4ab90db07f1609ccf06a287d5607fcd90dc9863acf", size = 323203, upload-time = "2025-10-06T14:11:54.225Z" }, + { url = "https://files.pythonhosted.org/packages/8c/96/475509110d3f0153b43d06164cf4195c64d16999e0c7e2d8a099adcd6907/yarl-1.22.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4647674b6150d2cae088fc07de2738a84b8bcedebef29802cf0b0a82ab6face", size = 363173, upload-time = "2025-10-06T14:11:56.069Z" }, + { url = "https://files.pythonhosted.org/packages/c9/66/59db471aecfbd559a1fd48aedd954435558cd98c7d0da8b03cc6c140a32c/yarl-1.22.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:efb07073be061c8f79d03d04139a80ba33cbd390ca8f0297aae9cce6411e4c6b", size = 373562, upload-time = "2025-10-06T14:11:58.783Z" }, + { url = "https://files.pythonhosted.org/packages/03/1f/c5d94abc91557384719da10ff166b916107c1b45e4d0423a88457071dd88/yarl-1.22.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e51ac5435758ba97ad69617e13233da53908beccc6cfcd6c34bbed8dcbede486", size = 339828, upload-time = "2025-10-06T14:12:00.686Z" }, + { url = "https://files.pythonhosted.org/packages/5f/97/aa6a143d3afba17b6465733681c70cf175af89f76ec8d9286e08437a7454/yarl-1.22.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:33e32a0dd0c8205efa8e83d04fc9f19313772b78522d1bdc7d9aed706bfd6138", size = 347551, upload-time = "2025-10-06T14:12:02.628Z" }, + { url = "https://files.pythonhosted.org/packages/43/3c/45a2b6d80195959239a7b2a8810506d4eea5487dce61c2a3393e7fc3c52e/yarl-1.22.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:bf4a21e58b9cde0e401e683ebd00f6ed30a06d14e93f7c8fd059f8b6e8f87b6a", size = 334512, upload-time = "2025-10-06T14:12:04.871Z" }, + { url = "https://files.pythonhosted.org/packages/86/a0/c2ab48d74599c7c84cb104ebd799c5813de252bea0f360ffc29d270c2caa/yarl-1.22.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:e4b582bab49ac33c8deb97e058cd67c2c50dac0dd134874106d9c774fd272529", size = 352400, upload-time = "2025-10-06T14:12:06.624Z" }, + { url = "https://files.pythonhosted.org/packages/32/75/f8919b2eafc929567d3d8411f72bdb1a2109c01caaab4ebfa5f8ffadc15b/yarl-1.22.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:0b5bcc1a9c4839e7e30b7b30dd47fe5e7e44fb7054ec29b5bb8d526aa1041093", size = 357140, upload-time = "2025-10-06T14:12:08.362Z" }, + { url = "https://files.pythonhosted.org/packages/cf/72/6a85bba382f22cf78add705d8c3731748397d986e197e53ecc7835e76de7/yarl-1.22.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c0232bce2170103ec23c454e54a57008a9a72b5d1c3105dc2496750da8cfa47c", size = 341473, upload-time = "2025-10-06T14:12:10.994Z" }, + { url = "https://files.pythonhosted.org/packages/35/18/55e6011f7c044dc80b98893060773cefcfdbf60dfefb8cb2f58b9bacbd83/yarl-1.22.0-cp314-cp314t-win32.whl", hash = "sha256:8009b3173bcd637be650922ac455946197d858b3630b6d8787aa9e5c4564533e", size = 89056, upload-time = "2025-10-06T14:12:13.317Z" }, + { url = "https://files.pythonhosted.org/packages/f9/86/0f0dccb6e59a9e7f122c5afd43568b1d31b8ab7dda5f1b01fb5c7025c9a9/yarl-1.22.0-cp314-cp314t-win_amd64.whl", hash = "sha256:9fb17ea16e972c63d25d4a97f016d235c78dd2344820eb35bc034bc32012ee27", size = 96292, upload-time = "2025-10-06T14:12:15.398Z" }, + { url = "https://files.pythonhosted.org/packages/48/b7/503c98092fb3b344a179579f55814b613c1fbb1c23b3ec14a7b008a66a6e/yarl-1.22.0-cp314-cp314t-win_arm64.whl", hash = "sha256:9f6d73c1436b934e3f01df1e1b21ff765cd1d28c77dfb9ace207f746d4610ee1", size = 85171, upload-time = "2025-10-06T14:12:16.935Z" }, + { url = "https://files.pythonhosted.org/packages/94/fd/6480106702a79bcceda5fd9c63cb19a04a6506bd5ce7fd8d9b63742f0021/yarl-1.22.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:3aa27acb6de7a23785d81557577491f6c38a5209a254d1191519d07d8fe51748", size = 141301, upload-time = "2025-10-06T14:12:19.01Z" }, + { url = "https://files.pythonhosted.org/packages/42/e1/6d95d21b17a93e793e4ec420a925fe1f6a9342338ca7a563ed21129c0990/yarl-1.22.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:af74f05666a5e531289cb1cc9c883d1de2088b8e5b4de48004e5ca8a830ac859", size = 93864, upload-time = "2025-10-06T14:12:21.05Z" }, + { url = "https://files.pythonhosted.org/packages/32/58/b8055273c203968e89808413ea4c984988b6649baabf10f4522e67c22d2f/yarl-1.22.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:62441e55958977b8167b2709c164c91a6363e25da322d87ae6dd9c6019ceecf9", size = 94706, upload-time = "2025-10-06T14:12:23.287Z" }, + { url = "https://files.pythonhosted.org/packages/18/91/d7bfbc28a88c2895ecd0da6a874def0c147de78afc52c773c28e1aa233a3/yarl-1.22.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b580e71cac3f8113d3135888770903eaf2f507e9421e5697d6ee6d8cd1c7f054", size = 347100, upload-time = "2025-10-06T14:12:28.527Z" }, + { url = "https://files.pythonhosted.org/packages/bd/e8/37a1e7b99721c0564b1fc7b0a4d1f595ef6fb8060d82ca61775b644185f7/yarl-1.22.0-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e81fda2fb4a07eda1a2252b216aa0df23ebcd4d584894e9612e80999a78fd95b", size = 318902, upload-time = "2025-10-06T14:12:30.528Z" }, + { url = "https://files.pythonhosted.org/packages/1c/ef/34724449d7ef2db4f22df644f2dac0b8a275d20f585e526937b3ae47b02d/yarl-1.22.0-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:99b6fc1d55782461b78221e95fc357b47ad98b041e8e20f47c1411d0aacddc60", size = 363302, upload-time = "2025-10-06T14:12:32.295Z" }, + { url = "https://files.pythonhosted.org/packages/8a/04/88a39a5dad39889f192cce8d66cc4c58dbeca983e83f9b6bf23822a7ed91/yarl-1.22.0-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:088e4e08f033db4be2ccd1f34cf29fe994772fb54cfe004bbf54db320af56890", size = 370816, upload-time = "2025-10-06T14:12:34.01Z" }, + { url = "https://files.pythonhosted.org/packages/6b/1f/5e895e547129413f56c76be2c3ce4b96c797d2d0ff3e16a817d9269b12e6/yarl-1.22.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2e4e1f6f0b4da23e61188676e3ed027ef0baa833a2e633c29ff8530800edccba", size = 346465, upload-time = "2025-10-06T14:12:35.977Z" }, + { url = "https://files.pythonhosted.org/packages/11/13/a750e9fd6f9cc9ed3a52a70fe58ffe505322f0efe0d48e1fd9ffe53281f5/yarl-1.22.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:84fc3ec96fce86ce5aa305eb4aa9358279d1aa644b71fab7b8ed33fe3ba1a7ca", size = 341506, upload-time = "2025-10-06T14:12:37.788Z" }, + { url = "https://files.pythonhosted.org/packages/3c/67/bb6024de76e7186611ebe626aec5b71a2d2ecf9453e795f2dbd80614784c/yarl-1.22.0-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:5dbeefd6ca588b33576a01b0ad58aa934bc1b41ef89dee505bf2932b22ddffba", size = 335030, upload-time = "2025-10-06T14:12:39.775Z" }, + { url = "https://files.pythonhosted.org/packages/a2/be/50b38447fd94a7992996a62b8b463d0579323fcfc08c61bdba949eef8a5d/yarl-1.22.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:14291620375b1060613f4aab9ebf21850058b6b1b438f386cc814813d901c60b", size = 358560, upload-time = "2025-10-06T14:12:41.547Z" }, + { url = "https://files.pythonhosted.org/packages/e2/89/c020b6f547578c4e3dbb6335bf918f26e2f34ad0d1e515d72fd33ac0c635/yarl-1.22.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:a4fcfc8eb2c34148c118dfa02e6427ca278bfd0f3df7c5f99e33d2c0e81eae3e", size = 357290, upload-time = "2025-10-06T14:12:43.861Z" }, + { url = "https://files.pythonhosted.org/packages/8c/52/c49a619ee35a402fa3a7019a4fa8d26878fec0d1243f6968bbf516789578/yarl-1.22.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:029866bde8d7b0878b9c160e72305bbf0a7342bcd20b9999381704ae03308dc8", size = 350700, upload-time = "2025-10-06T14:12:46.868Z" }, + { url = "https://files.pythonhosted.org/packages/ab/c9/f5042d87777bf6968435f04a2bbb15466b2f142e6e47fa4f34d1a3f32f0c/yarl-1.22.0-cp39-cp39-win32.whl", hash = "sha256:4dcc74149ccc8bba31ce1944acee24813e93cfdee2acda3c172df844948ddf7b", size = 82323, upload-time = "2025-10-06T14:12:48.633Z" }, + { url = "https://files.pythonhosted.org/packages/fd/58/d00f7cad9eba20c4eefac2682f34661d1d1b3a942fc0092eb60e78cfb733/yarl-1.22.0-cp39-cp39-win_amd64.whl", hash = "sha256:10619d9fdee46d20edc49d3479e2f8269d0779f1b031e6f7c2aa1c76be04b7ed", size = 87145, upload-time = "2025-10-06T14:12:50.241Z" }, + { url = "https://files.pythonhosted.org/packages/c2/a3/70904f365080780d38b919edd42d224b8c4ce224a86950d2eaa2a24366ad/yarl-1.22.0-cp39-cp39-win_arm64.whl", hash = "sha256:dd7afd3f8b0bfb4e0d9fc3c31bfe8a4ec7debe124cfd90619305def3c8ca8cd2", size = 82173, upload-time = "2025-10-06T14:12:51.869Z" }, + { url = "https://files.pythonhosted.org/packages/73/ae/b48f95715333080afb75a4504487cbe142cae1268afc482d06692d605ae6/yarl-1.22.0-py3-none-any.whl", hash = "sha256:1380560bdba02b6b6c90de54133c81c9f2a453dee9912fe58c1dcced1edb7cff", size = 46814, upload-time = "2025-10-06T14:12:53.872Z" }, +] + +[[package]] +name = "zipp" +version = "3.23.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e3/02/0f2892c661036d50ede074e376733dca2ae7c6eb617489437771209d4180/zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166", size = 25547, upload-time = "2025-06-08T17:06:39.4Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", size = 10276, upload-time = "2025-06-08T17:06:38.034Z" }, +] From 4a17fa1924bc78639726fe04c6068361396ad2cb Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 17 Dec 2025 18:20:43 +0000 Subject: [PATCH 32/88] feat(api): manual updates --- .stats.yml | 6 +- README.md | 64 +- api.md | 26 +- src/stagehand/resources/sessions.py | 733 ++++++++++++---- src/stagehand/types/__init__.py | 11 +- src/stagehand/types/action.py | 23 + src/stagehand/types/model_config_param.py | 21 + src/stagehand/types/session_act_params.py | 57 +- src/stagehand/types/session_act_response.py | 38 + src/stagehand/types/session_end_response.py | 11 + .../types/session_execute_agent_params.py | 21 - src/stagehand/types/session_execute_params.py | 54 ++ .../types/session_execute_response.py | 82 ++ src/stagehand/types/session_extract_params.py | 40 +- .../types/session_extract_response.py | 24 + .../types/session_navigate_params.py | 37 +- .../types/session_navigate_response.py | 24 + src/stagehand/types/session_observe_params.py | 37 +- .../types/session_observe_response.py | 24 + src/stagehand/types/session_start_params.py | 242 +++++- src/stagehand/types/session_start_response.py | 22 + tests/api_resources/test_sessions.py | 782 +++++++++++++----- tests/test_client.py | 72 +- 23 files changed, 1986 insertions(+), 465 deletions(-) create mode 100644 src/stagehand/types/action.py create mode 100644 src/stagehand/types/model_config_param.py create mode 100644 src/stagehand/types/session_act_response.py create mode 100644 src/stagehand/types/session_end_response.py delete mode 100644 src/stagehand/types/session_execute_agent_params.py create mode 100644 src/stagehand/types/session_execute_params.py create mode 100644 src/stagehand/types/session_execute_response.py create mode 100644 src/stagehand/types/session_extract_response.py create mode 100644 src/stagehand/types/session_navigate_response.py create mode 100644 src/stagehand/types/session_observe_response.py create mode 100644 src/stagehand/types/session_start_response.py diff --git a/.stats.yml b/.stats.yml index c436edec..87bd24b5 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 7 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fstagehand-c1a6f03afe5d6823c198e5ac476fb688dacc783dae1fefdf6bf142084e298e16.yml -openapi_spec_hash: d20e8f697ce8d5bb80295fc1e8ce02e8 -config_hash: e457d704d820df5d25acfd379169f132 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fstagehand-349e1b0f6291eedd731c1660155a50adcb3424fb8cd9e17bbdc0939ff3bbffcd.yml +openapi_spec_hash: 456b593ea71d72bc31a6338a25363e9f +config_hash: 5f6b5ec6e84fb01932ba87c6a9623d9b diff --git a/README.md b/README.md index 443970e4..84c60a60 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,11 @@ client = Stagehand( model_api_key=os.environ.get("MODEL_API_KEY"), # This is the default and can be omitted ) -response = client.sessions.start() +response = client.sessions.act( + id="c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123", + input="Click the login button", +) +print(response.data) ``` While you can provide a `browserbase_api_key` keyword argument, @@ -70,7 +74,11 @@ client = AsyncStagehand( async def main() -> None: - response = await client.sessions.start() + response = await client.sessions.act( + id="c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123", + input="Click the login button", + ) + print(response.data) asyncio.run(main()) @@ -109,7 +117,11 @@ async def main() -> None: model_api_key=os.environ.get("MODEL_API_KEY"), # This is the default and can be omitted http_client=DefaultAioHttpClient(), ) as client: - response = await client.sessions.start() + response = await client.sessions.act( + id="c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123", + input="Click the login button", + ) + print(response.data) asyncio.run(main()) @@ -124,6 +136,23 @@ Nested request parameters are [TypedDicts](https://docs.python.org/3/library/typ Typed requests and responses provide autocomplete and documentation within your editor. If you would like to see type errors in VS Code to help catch bugs earlier, set `python.analysis.typeCheckingMode` to `basic`. +## Nested params + +Nested parameters are dictionaries, typed using `TypedDict`, for example: + +```python +from stagehand import Stagehand + +client = Stagehand() + +response = client.sessions.act( + id="c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123", + input="Click the login button", + options={}, +) +print(response.options) +``` + ## Handling errors When the library is unable to connect to the API (for example, due to network connection problems or a timeout), a subclass of `stagehand.APIConnectionError` is raised. @@ -140,7 +169,10 @@ from stagehand import Stagehand client = Stagehand() try: - client.sessions.start() + client.sessions.act( + id="c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123", + input="Click the login button", + ) except stagehand.APIConnectionError as e: print("The server could not be reached") print(e.__cause__) # an underlying Exception, likely raised within httpx. @@ -183,7 +215,10 @@ client = Stagehand( ) # Or, configure per-request: -client.with_options(max_retries=5).sessions.start() +client.with_options(max_retries=5).sessions.act( + id="c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123", + input="Click the login button", +) ``` ### Timeouts @@ -206,7 +241,10 @@ client = Stagehand( ) # Override per-request: -client.with_options(timeout=5.0).sessions.start() +client.with_options(timeout=5.0).sessions.act( + id="c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123", + input="Click the login button", +) ``` On timeout, an `APITimeoutError` is thrown. @@ -247,11 +285,14 @@ The "raw" Response object can be accessed by prefixing `.with_raw_response.` to from stagehand import Stagehand client = Stagehand() -response = client.sessions.with_raw_response.start() +response = client.sessions.with_raw_response.act( + id="c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123", + input="Click the login button", +) print(response.headers.get('X-My-Header')) -session = response.parse() # get the object that `sessions.start()` would have returned -print(session) +session = response.parse() # get the object that `sessions.act()` would have returned +print(session.data) ``` These methods return an [`APIResponse`](https://github.com/browserbase/stagehand-python/tree/stainless/src/stagehand/_response.py) object. @@ -265,7 +306,10 @@ The above interface eagerly reads the full response body when you make the reque To stream the response body, use `.with_streaming_response` instead, which requires a context manager and only reads the response body once you call `.read()`, `.text()`, `.json()`, `.iter_bytes()`, `.iter_text()`, `.iter_lines()` or `.parse()`. In the async client, these are async methods. ```python -with client.sessions.with_streaming_response.start() as response: +with client.sessions.with_streaming_response.act( + id="c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123", + input="Click the login button", +) as response: print(response.headers.get("X-My-Header")) for line in response.iter_lines(): diff --git a/api.md b/api.md index f09809b4..cc172ac8 100644 --- a/api.md +++ b/api.md @@ -3,15 +3,25 @@ Types: ```python -from stagehand.types import Action, ModelConfig +from stagehand.types import ( + Action, + ModelConfig, + SessionActResponse, + SessionEndResponse, + SessionExecuteResponse, + SessionExtractResponse, + SessionNavigateResponse, + SessionObserveResponse, + SessionStartResponse, +) ``` Methods: -- client.sessions.act(id, \*\*params) -> object -- client.sessions.end(id) -> object -- client.sessions.execute_agent(id, \*\*params) -> object -- client.sessions.extract(id, \*\*params) -> object -- client.sessions.navigate(id, \*\*params) -> object -- client.sessions.observe(id, \*\*params) -> object -- client.sessions.start(\*\*params) -> object +- client.sessions.act(id, \*\*params) -> SessionActResponse +- client.sessions.end(id) -> SessionEndResponse +- client.sessions.execute(id, \*\*params) -> SessionExecuteResponse +- client.sessions.extract(id, \*\*params) -> SessionExtractResponse +- client.sessions.navigate(id, \*\*params) -> SessionNavigateResponse +- client.sessions.observe(id, \*\*params) -> SessionObserveResponse +- client.sessions.start(\*\*params) -> SessionStartResponse diff --git a/src/stagehand/resources/sessions.py b/src/stagehand/resources/sessions.py index c98fbc23..a39547dc 100644 --- a/src/stagehand/resources/sessions.py +++ b/src/stagehand/resources/sessions.py @@ -2,15 +2,19 @@ from __future__ import annotations +from typing import Dict, Union +from datetime import datetime +from typing_extensions import Literal + import httpx from ..types import ( session_act_params, session_start_params, + session_execute_params, session_extract_params, session_observe_params, session_navigate_params, - session_execute_agent_params, ) from .._types import Body, Omit, Query, Headers, NotGiven, omit, not_given from .._utils import is_given, maybe_transform, strip_not_given, async_maybe_transform @@ -23,6 +27,13 @@ async_to_streamed_response_wrapper, ) from .._base_client import make_request_options +from ..types.session_act_response import SessionActResponse +from ..types.session_end_response import SessionEndResponse +from ..types.session_start_response import SessionStartResponse +from ..types.session_execute_response import SessionExecuteResponse +from ..types.session_extract_response import SessionExtractResponse +from ..types.session_observe_response import SessionObserveResponse +from ..types.session_navigate_response import SessionNavigateResponse __all__ = ["SessionsResource", "AsyncSessionsResource"] @@ -49,25 +60,41 @@ def with_streaming_response(self) -> SessionsResourceWithStreamingResponse: def act( self, - id: object, + id: str, *, - body: object | Omit = omit, - x_language: object | Omit = omit, - x_sdk_version: object | Omit = omit, - x_sent_at: object | Omit = omit, - x_stream_response: object | Omit = omit, + input: session_act_params.Input, + frame_id: str | Omit = omit, + options: session_act_params.Options | Omit = omit, + x_language: Literal["typescript", "python", "playground"] | Omit = omit, + x_sdk_version: str | Omit = omit, + x_sent_at: Union[str, datetime] | Omit = omit, + x_stream_response: Literal["true", "false"] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> object: + ) -> SessionActResponse: """ Executes a browser action using natural language instructions or a predefined Action object. Args: + id: Unique session identifier + + input: Natural language instruction or Action object + + frame_id: Target frame ID for the action + + x_language: Client SDK language + + x_sdk_version: Version of the Stagehand SDK + + x_sent_at: ISO timestamp when request was sent + + x_stream_response: Whether to stream the response via SSE + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -76,45 +103,64 @@ def act( timeout: Override the client-level default timeout for this request, in seconds """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") extra_headers = { **strip_not_given( { "x-language": str(x_language) if is_given(x_language) else not_given, - "x-sdk-version": str(x_sdk_version) if is_given(x_sdk_version) else not_given, - "x-sent-at": str(x_sent_at) if is_given(x_sent_at) else not_given, + "x-sdk-version": x_sdk_version, + "x-sent-at": x_sent_at.isoformat() if is_given(x_sent_at) else not_given, "x-stream-response": str(x_stream_response) if is_given(x_stream_response) else not_given, } ), **(extra_headers or {}), } return self._post( - f"/sessions/{id}/act", - body=maybe_transform(body, session_act_params.SessionActParams), + f"/v1/sessions/{id}/act", + body=maybe_transform( + { + "input": input, + "frame_id": frame_id, + "options": options, + }, + session_act_params.SessionActParams, + ), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=object, + cast_to=SessionActResponse, ) def end( self, - id: object, + id: str, *, - x_language: object | Omit = omit, - x_sdk_version: object | Omit = omit, - x_sent_at: object | Omit = omit, - x_stream_response: object | Omit = omit, + x_language: Literal["typescript", "python", "playground"] | Omit = omit, + x_sdk_version: str | Omit = omit, + x_sent_at: Union[str, datetime] | Omit = omit, + x_stream_response: Literal["true", "false"] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> object: + ) -> SessionEndResponse: """ Terminates the browser session and releases all associated resources. Args: + id: Unique session identifier + + x_language: Client SDK language + + x_sdk_version: Version of the Stagehand SDK + + x_sent_at: ISO timestamp when request was sent + + x_stream_response: Whether to stream the response via SSE + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -123,45 +169,61 @@ def end( timeout: Override the client-level default timeout for this request, in seconds """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") extra_headers = { **strip_not_given( { "x-language": str(x_language) if is_given(x_language) else not_given, - "x-sdk-version": str(x_sdk_version) if is_given(x_sdk_version) else not_given, - "x-sent-at": str(x_sent_at) if is_given(x_sent_at) else not_given, + "x-sdk-version": x_sdk_version, + "x-sent-at": x_sent_at.isoformat() if is_given(x_sent_at) else not_given, "x-stream-response": str(x_stream_response) if is_given(x_stream_response) else not_given, } ), **(extra_headers or {}), } return self._post( - f"/sessions/{id}/end", + f"/v1/sessions/{id}/end", options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=object, + cast_to=SessionEndResponse, ) - def execute_agent( + def execute( self, - id: object, + id: str, *, - body: object | Omit = omit, - x_language: object | Omit = omit, - x_sdk_version: object | Omit = omit, - x_sent_at: object | Omit = omit, - x_stream_response: object | Omit = omit, + agent_config: session_execute_params.AgentConfig, + execute_options: session_execute_params.ExecuteOptions, + frame_id: str | Omit = omit, + x_language: Literal["typescript", "python", "playground"] | Omit = omit, + x_sdk_version: str | Omit = omit, + x_sent_at: Union[str, datetime] | Omit = omit, + x_stream_response: Literal["true", "false"] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> object: + ) -> SessionExecuteResponse: """ Runs an autonomous AI agent that can perform complex multi-step browser tasks. Args: + id: Unique session identifier + + frame_id: Target frame ID for the agent + + x_language: Client SDK language + + x_sdk_version: Version of the Stagehand SDK + + x_sent_at: ISO timestamp when request was sent + + x_stream_response: Whether to stream the response via SSE + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -170,46 +232,74 @@ def execute_agent( timeout: Override the client-level default timeout for this request, in seconds """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") extra_headers = { **strip_not_given( { "x-language": str(x_language) if is_given(x_language) else not_given, - "x-sdk-version": str(x_sdk_version) if is_given(x_sdk_version) else not_given, - "x-sent-at": str(x_sent_at) if is_given(x_sent_at) else not_given, + "x-sdk-version": x_sdk_version, + "x-sent-at": x_sent_at.isoformat() if is_given(x_sent_at) else not_given, "x-stream-response": str(x_stream_response) if is_given(x_stream_response) else not_given, } ), **(extra_headers or {}), } return self._post( - f"/sessions/{id}/agentExecute", - body=maybe_transform(body, session_execute_agent_params.SessionExecuteAgentParams), + f"/v1/sessions/{id}/agentExecute", + body=maybe_transform( + { + "agent_config": agent_config, + "execute_options": execute_options, + "frame_id": frame_id, + }, + session_execute_params.SessionExecuteParams, + ), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=object, + cast_to=SessionExecuteResponse, ) def extract( self, - id: object, + id: str, *, - body: object | Omit = omit, - x_language: object | Omit = omit, - x_sdk_version: object | Omit = omit, - x_sent_at: object | Omit = omit, - x_stream_response: object | Omit = omit, + frame_id: str | Omit = omit, + instruction: str | Omit = omit, + options: session_extract_params.Options | Omit = omit, + schema: Dict[str, object] | Omit = omit, + x_language: Literal["typescript", "python", "playground"] | Omit = omit, + x_sdk_version: str | Omit = omit, + x_sent_at: Union[str, datetime] | Omit = omit, + x_stream_response: Literal["true", "false"] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> object: + ) -> SessionExtractResponse: """ Extracts structured data from the current page using AI-powered analysis. Args: + id: Unique session identifier + + frame_id: Target frame ID for the extraction + + instruction: Natural language instruction for what to extract + + schema: JSON Schema defining the structure of data to extract + + x_language: Client SDK language + + x_sdk_version: Version of the Stagehand SDK + + x_sent_at: ISO timestamp when request was sent + + x_stream_response: Whether to stream the response via SSE + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -218,46 +308,72 @@ def extract( timeout: Override the client-level default timeout for this request, in seconds """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") extra_headers = { **strip_not_given( { "x-language": str(x_language) if is_given(x_language) else not_given, - "x-sdk-version": str(x_sdk_version) if is_given(x_sdk_version) else not_given, - "x-sent-at": str(x_sent_at) if is_given(x_sent_at) else not_given, + "x-sdk-version": x_sdk_version, + "x-sent-at": x_sent_at.isoformat() if is_given(x_sent_at) else not_given, "x-stream-response": str(x_stream_response) if is_given(x_stream_response) else not_given, } ), **(extra_headers or {}), } return self._post( - f"/sessions/{id}/extract", - body=maybe_transform(body, session_extract_params.SessionExtractParams), + f"/v1/sessions/{id}/extract", + body=maybe_transform( + { + "frame_id": frame_id, + "instruction": instruction, + "options": options, + "schema": schema, + }, + session_extract_params.SessionExtractParams, + ), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=object, + cast_to=SessionExtractResponse, ) def navigate( self, - id: object, + id: str, *, - body: object | Omit = omit, - x_language: object | Omit = omit, - x_sdk_version: object | Omit = omit, - x_sent_at: object | Omit = omit, - x_stream_response: object | Omit = omit, + url: str, + frame_id: str | Omit = omit, + options: session_navigate_params.Options | Omit = omit, + x_language: Literal["typescript", "python", "playground"] | Omit = omit, + x_sdk_version: str | Omit = omit, + x_sent_at: Union[str, datetime] | Omit = omit, + x_stream_response: Literal["true", "false"] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> object: + ) -> SessionNavigateResponse: """ Navigates the browser to the specified URL. Args: + id: Unique session identifier + + url: URL to navigate to + + frame_id: Target frame ID for the navigation + + x_language: Client SDK language + + x_sdk_version: Version of the Stagehand SDK + + x_sent_at: ISO timestamp when request was sent + + x_stream_response: Whether to stream the response via SSE + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -266,47 +382,72 @@ def navigate( timeout: Override the client-level default timeout for this request, in seconds """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") extra_headers = { **strip_not_given( { "x-language": str(x_language) if is_given(x_language) else not_given, - "x-sdk-version": str(x_sdk_version) if is_given(x_sdk_version) else not_given, - "x-sent-at": str(x_sent_at) if is_given(x_sent_at) else not_given, + "x-sdk-version": x_sdk_version, + "x-sent-at": x_sent_at.isoformat() if is_given(x_sent_at) else not_given, "x-stream-response": str(x_stream_response) if is_given(x_stream_response) else not_given, } ), **(extra_headers or {}), } return self._post( - f"/sessions/{id}/navigate", - body=maybe_transform(body, session_navigate_params.SessionNavigateParams), + f"/v1/sessions/{id}/navigate", + body=maybe_transform( + { + "url": url, + "frame_id": frame_id, + "options": options, + }, + session_navigate_params.SessionNavigateParams, + ), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=object, + cast_to=SessionNavigateResponse, ) def observe( self, - id: object, + id: str, *, - body: object | Omit = omit, - x_language: object | Omit = omit, - x_sdk_version: object | Omit = omit, - x_sent_at: object | Omit = omit, - x_stream_response: object | Omit = omit, + frame_id: str | Omit = omit, + instruction: str | Omit = omit, + options: session_observe_params.Options | Omit = omit, + x_language: Literal["typescript", "python", "playground"] | Omit = omit, + x_sdk_version: str | Omit = omit, + x_sent_at: Union[str, datetime] | Omit = omit, + x_stream_response: Literal["true", "false"] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> object: + ) -> SessionObserveResponse: """ Identifies and returns available actions on the current page that match the given instruction. Args: + id: Unique session identifier + + frame_id: Target frame ID for the observation + + instruction: Natural language instruction for what actions to find + + x_language: Client SDK language + + x_sdk_version: Version of the Stagehand SDK + + x_sent_at: ISO timestamp when request was sent + + x_stream_response: Whether to stream the response via SSE + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -315,47 +456,89 @@ def observe( timeout: Override the client-level default timeout for this request, in seconds """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") extra_headers = { **strip_not_given( { "x-language": str(x_language) if is_given(x_language) else not_given, - "x-sdk-version": str(x_sdk_version) if is_given(x_sdk_version) else not_given, - "x-sent-at": str(x_sent_at) if is_given(x_sent_at) else not_given, + "x-sdk-version": x_sdk_version, + "x-sent-at": x_sent_at.isoformat() if is_given(x_sent_at) else not_given, "x-stream-response": str(x_stream_response) if is_given(x_stream_response) else not_given, } ), **(extra_headers or {}), } return self._post( - f"/sessions/{id}/observe", - body=maybe_transform(body, session_observe_params.SessionObserveParams), + f"/v1/sessions/{id}/observe", + body=maybe_transform( + { + "frame_id": frame_id, + "instruction": instruction, + "options": options, + }, + session_observe_params.SessionObserveParams, + ), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=object, + cast_to=SessionObserveResponse, ) def start( self, *, - body: object | Omit = omit, - x_language: object | Omit = omit, - x_sdk_version: object | Omit = omit, - x_sent_at: object | Omit = omit, - x_stream_response: object | Omit = omit, + model_name: str, + act_timeout_ms: float | Omit = omit, + browser: session_start_params.Browser | Omit = omit, + browserbase_session_create_params: session_start_params.BrowserbaseSessionCreateParams | Omit = omit, + browserbase_session_id: str | Omit = omit, + debug_dom: bool | Omit = omit, + dom_settle_timeout_ms: float | Omit = omit, + experimental: bool | Omit = omit, + self_heal: bool | Omit = omit, + system_prompt: str | Omit = omit, + verbose: Literal[0, 1, 2] | Omit = omit, + wait_for_captcha_solves: bool | Omit = omit, + x_language: Literal["typescript", "python", "playground"] | Omit = omit, + x_sdk_version: str | Omit = omit, + x_sent_at: Union[str, datetime] | Omit = omit, + x_stream_response: Literal["true", "false"] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> object: + ) -> SessionStartResponse: """Creates a new browser session with the specified configuration. Returns a session ID used for all subsequent operations. Args: + model_name: Model name to use for AI operations + + act_timeout_ms: Timeout in ms for act operations + + browserbase_session_id: Existing Browserbase session ID to resume + + dom_settle_timeout_ms: Timeout in ms to wait for DOM to settle + + self_heal: Enable self-healing for failed actions + + system_prompt: Custom system prompt for AI operations + + verbose: Logging verbosity level (0=quiet, 1=normal, 2=debug) + + x_language: Client SDK language + + x_sdk_version: Version of the Stagehand SDK + + x_sent_at: ISO timestamp when request was sent + + x_stream_response: Whether to stream the response via SSE + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -368,20 +551,36 @@ def start( **strip_not_given( { "x-language": str(x_language) if is_given(x_language) else not_given, - "x-sdk-version": str(x_sdk_version) if is_given(x_sdk_version) else not_given, - "x-sent-at": str(x_sent_at) if is_given(x_sent_at) else not_given, + "x-sdk-version": x_sdk_version, + "x-sent-at": x_sent_at.isoformat() if is_given(x_sent_at) else not_given, "x-stream-response": str(x_stream_response) if is_given(x_stream_response) else not_given, } ), **(extra_headers or {}), } return self._post( - "/sessions/start", - body=maybe_transform(body, session_start_params.SessionStartParams), + "/v1/sessions/start", + body=maybe_transform( + { + "model_name": model_name, + "act_timeout_ms": act_timeout_ms, + "browser": browser, + "browserbase_session_create_params": browserbase_session_create_params, + "browserbase_session_id": browserbase_session_id, + "debug_dom": debug_dom, + "dom_settle_timeout_ms": dom_settle_timeout_ms, + "experimental": experimental, + "self_heal": self_heal, + "system_prompt": system_prompt, + "verbose": verbose, + "wait_for_captcha_solves": wait_for_captcha_solves, + }, + session_start_params.SessionStartParams, + ), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=object, + cast_to=SessionStartResponse, ) @@ -407,25 +606,41 @@ def with_streaming_response(self) -> AsyncSessionsResourceWithStreamingResponse: async def act( self, - id: object, + id: str, *, - body: object | Omit = omit, - x_language: object | Omit = omit, - x_sdk_version: object | Omit = omit, - x_sent_at: object | Omit = omit, - x_stream_response: object | Omit = omit, + input: session_act_params.Input, + frame_id: str | Omit = omit, + options: session_act_params.Options | Omit = omit, + x_language: Literal["typescript", "python", "playground"] | Omit = omit, + x_sdk_version: str | Omit = omit, + x_sent_at: Union[str, datetime] | Omit = omit, + x_stream_response: Literal["true", "false"] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> object: + ) -> SessionActResponse: """ Executes a browser action using natural language instructions or a predefined Action object. Args: + id: Unique session identifier + + input: Natural language instruction or Action object + + frame_id: Target frame ID for the action + + x_language: Client SDK language + + x_sdk_version: Version of the Stagehand SDK + + x_sent_at: ISO timestamp when request was sent + + x_stream_response: Whether to stream the response via SSE + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -434,45 +649,64 @@ async def act( timeout: Override the client-level default timeout for this request, in seconds """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") extra_headers = { **strip_not_given( { "x-language": str(x_language) if is_given(x_language) else not_given, - "x-sdk-version": str(x_sdk_version) if is_given(x_sdk_version) else not_given, - "x-sent-at": str(x_sent_at) if is_given(x_sent_at) else not_given, + "x-sdk-version": x_sdk_version, + "x-sent-at": x_sent_at.isoformat() if is_given(x_sent_at) else not_given, "x-stream-response": str(x_stream_response) if is_given(x_stream_response) else not_given, } ), **(extra_headers or {}), } return await self._post( - f"/sessions/{id}/act", - body=await async_maybe_transform(body, session_act_params.SessionActParams), + f"/v1/sessions/{id}/act", + body=await async_maybe_transform( + { + "input": input, + "frame_id": frame_id, + "options": options, + }, + session_act_params.SessionActParams, + ), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=object, + cast_to=SessionActResponse, ) async def end( self, - id: object, + id: str, *, - x_language: object | Omit = omit, - x_sdk_version: object | Omit = omit, - x_sent_at: object | Omit = omit, - x_stream_response: object | Omit = omit, + x_language: Literal["typescript", "python", "playground"] | Omit = omit, + x_sdk_version: str | Omit = omit, + x_sent_at: Union[str, datetime] | Omit = omit, + x_stream_response: Literal["true", "false"] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> object: + ) -> SessionEndResponse: """ Terminates the browser session and releases all associated resources. Args: + id: Unique session identifier + + x_language: Client SDK language + + x_sdk_version: Version of the Stagehand SDK + + x_sent_at: ISO timestamp when request was sent + + x_stream_response: Whether to stream the response via SSE + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -481,45 +715,61 @@ async def end( timeout: Override the client-level default timeout for this request, in seconds """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") extra_headers = { **strip_not_given( { "x-language": str(x_language) if is_given(x_language) else not_given, - "x-sdk-version": str(x_sdk_version) if is_given(x_sdk_version) else not_given, - "x-sent-at": str(x_sent_at) if is_given(x_sent_at) else not_given, + "x-sdk-version": x_sdk_version, + "x-sent-at": x_sent_at.isoformat() if is_given(x_sent_at) else not_given, "x-stream-response": str(x_stream_response) if is_given(x_stream_response) else not_given, } ), **(extra_headers or {}), } return await self._post( - f"/sessions/{id}/end", + f"/v1/sessions/{id}/end", options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=object, + cast_to=SessionEndResponse, ) - async def execute_agent( + async def execute( self, - id: object, + id: str, *, - body: object | Omit = omit, - x_language: object | Omit = omit, - x_sdk_version: object | Omit = omit, - x_sent_at: object | Omit = omit, - x_stream_response: object | Omit = omit, + agent_config: session_execute_params.AgentConfig, + execute_options: session_execute_params.ExecuteOptions, + frame_id: str | Omit = omit, + x_language: Literal["typescript", "python", "playground"] | Omit = omit, + x_sdk_version: str | Omit = omit, + x_sent_at: Union[str, datetime] | Omit = omit, + x_stream_response: Literal["true", "false"] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> object: + ) -> SessionExecuteResponse: """ Runs an autonomous AI agent that can perform complex multi-step browser tasks. Args: + id: Unique session identifier + + frame_id: Target frame ID for the agent + + x_language: Client SDK language + + x_sdk_version: Version of the Stagehand SDK + + x_sent_at: ISO timestamp when request was sent + + x_stream_response: Whether to stream the response via SSE + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -528,46 +778,74 @@ async def execute_agent( timeout: Override the client-level default timeout for this request, in seconds """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") extra_headers = { **strip_not_given( { "x-language": str(x_language) if is_given(x_language) else not_given, - "x-sdk-version": str(x_sdk_version) if is_given(x_sdk_version) else not_given, - "x-sent-at": str(x_sent_at) if is_given(x_sent_at) else not_given, + "x-sdk-version": x_sdk_version, + "x-sent-at": x_sent_at.isoformat() if is_given(x_sent_at) else not_given, "x-stream-response": str(x_stream_response) if is_given(x_stream_response) else not_given, } ), **(extra_headers or {}), } return await self._post( - f"/sessions/{id}/agentExecute", - body=await async_maybe_transform(body, session_execute_agent_params.SessionExecuteAgentParams), + f"/v1/sessions/{id}/agentExecute", + body=await async_maybe_transform( + { + "agent_config": agent_config, + "execute_options": execute_options, + "frame_id": frame_id, + }, + session_execute_params.SessionExecuteParams, + ), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=object, + cast_to=SessionExecuteResponse, ) async def extract( self, - id: object, + id: str, *, - body: object | Omit = omit, - x_language: object | Omit = omit, - x_sdk_version: object | Omit = omit, - x_sent_at: object | Omit = omit, - x_stream_response: object | Omit = omit, + frame_id: str | Omit = omit, + instruction: str | Omit = omit, + options: session_extract_params.Options | Omit = omit, + schema: Dict[str, object] | Omit = omit, + x_language: Literal["typescript", "python", "playground"] | Omit = omit, + x_sdk_version: str | Omit = omit, + x_sent_at: Union[str, datetime] | Omit = omit, + x_stream_response: Literal["true", "false"] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> object: + ) -> SessionExtractResponse: """ Extracts structured data from the current page using AI-powered analysis. Args: + id: Unique session identifier + + frame_id: Target frame ID for the extraction + + instruction: Natural language instruction for what to extract + + schema: JSON Schema defining the structure of data to extract + + x_language: Client SDK language + + x_sdk_version: Version of the Stagehand SDK + + x_sent_at: ISO timestamp when request was sent + + x_stream_response: Whether to stream the response via SSE + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -576,46 +854,72 @@ async def extract( timeout: Override the client-level default timeout for this request, in seconds """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") extra_headers = { **strip_not_given( { "x-language": str(x_language) if is_given(x_language) else not_given, - "x-sdk-version": str(x_sdk_version) if is_given(x_sdk_version) else not_given, - "x-sent-at": str(x_sent_at) if is_given(x_sent_at) else not_given, + "x-sdk-version": x_sdk_version, + "x-sent-at": x_sent_at.isoformat() if is_given(x_sent_at) else not_given, "x-stream-response": str(x_stream_response) if is_given(x_stream_response) else not_given, } ), **(extra_headers or {}), } return await self._post( - f"/sessions/{id}/extract", - body=await async_maybe_transform(body, session_extract_params.SessionExtractParams), + f"/v1/sessions/{id}/extract", + body=await async_maybe_transform( + { + "frame_id": frame_id, + "instruction": instruction, + "options": options, + "schema": schema, + }, + session_extract_params.SessionExtractParams, + ), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=object, + cast_to=SessionExtractResponse, ) async def navigate( self, - id: object, + id: str, *, - body: object | Omit = omit, - x_language: object | Omit = omit, - x_sdk_version: object | Omit = omit, - x_sent_at: object | Omit = omit, - x_stream_response: object | Omit = omit, + url: str, + frame_id: str | Omit = omit, + options: session_navigate_params.Options | Omit = omit, + x_language: Literal["typescript", "python", "playground"] | Omit = omit, + x_sdk_version: str | Omit = omit, + x_sent_at: Union[str, datetime] | Omit = omit, + x_stream_response: Literal["true", "false"] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> object: + ) -> SessionNavigateResponse: """ Navigates the browser to the specified URL. Args: + id: Unique session identifier + + url: URL to navigate to + + frame_id: Target frame ID for the navigation + + x_language: Client SDK language + + x_sdk_version: Version of the Stagehand SDK + + x_sent_at: ISO timestamp when request was sent + + x_stream_response: Whether to stream the response via SSE + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -624,47 +928,72 @@ async def navigate( timeout: Override the client-level default timeout for this request, in seconds """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") extra_headers = { **strip_not_given( { "x-language": str(x_language) if is_given(x_language) else not_given, - "x-sdk-version": str(x_sdk_version) if is_given(x_sdk_version) else not_given, - "x-sent-at": str(x_sent_at) if is_given(x_sent_at) else not_given, + "x-sdk-version": x_sdk_version, + "x-sent-at": x_sent_at.isoformat() if is_given(x_sent_at) else not_given, "x-stream-response": str(x_stream_response) if is_given(x_stream_response) else not_given, } ), **(extra_headers or {}), } return await self._post( - f"/sessions/{id}/navigate", - body=await async_maybe_transform(body, session_navigate_params.SessionNavigateParams), + f"/v1/sessions/{id}/navigate", + body=await async_maybe_transform( + { + "url": url, + "frame_id": frame_id, + "options": options, + }, + session_navigate_params.SessionNavigateParams, + ), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=object, + cast_to=SessionNavigateResponse, ) async def observe( self, - id: object, + id: str, *, - body: object | Omit = omit, - x_language: object | Omit = omit, - x_sdk_version: object | Omit = omit, - x_sent_at: object | Omit = omit, - x_stream_response: object | Omit = omit, + frame_id: str | Omit = omit, + instruction: str | Omit = omit, + options: session_observe_params.Options | Omit = omit, + x_language: Literal["typescript", "python", "playground"] | Omit = omit, + x_sdk_version: str | Omit = omit, + x_sent_at: Union[str, datetime] | Omit = omit, + x_stream_response: Literal["true", "false"] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> object: + ) -> SessionObserveResponse: """ Identifies and returns available actions on the current page that match the given instruction. Args: + id: Unique session identifier + + frame_id: Target frame ID for the observation + + instruction: Natural language instruction for what actions to find + + x_language: Client SDK language + + x_sdk_version: Version of the Stagehand SDK + + x_sent_at: ISO timestamp when request was sent + + x_stream_response: Whether to stream the response via SSE + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -673,47 +1002,89 @@ async def observe( timeout: Override the client-level default timeout for this request, in seconds """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") extra_headers = { **strip_not_given( { "x-language": str(x_language) if is_given(x_language) else not_given, - "x-sdk-version": str(x_sdk_version) if is_given(x_sdk_version) else not_given, - "x-sent-at": str(x_sent_at) if is_given(x_sent_at) else not_given, + "x-sdk-version": x_sdk_version, + "x-sent-at": x_sent_at.isoformat() if is_given(x_sent_at) else not_given, "x-stream-response": str(x_stream_response) if is_given(x_stream_response) else not_given, } ), **(extra_headers or {}), } return await self._post( - f"/sessions/{id}/observe", - body=await async_maybe_transform(body, session_observe_params.SessionObserveParams), + f"/v1/sessions/{id}/observe", + body=await async_maybe_transform( + { + "frame_id": frame_id, + "instruction": instruction, + "options": options, + }, + session_observe_params.SessionObserveParams, + ), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=object, + cast_to=SessionObserveResponse, ) async def start( self, *, - body: object | Omit = omit, - x_language: object | Omit = omit, - x_sdk_version: object | Omit = omit, - x_sent_at: object | Omit = omit, - x_stream_response: object | Omit = omit, + model_name: str, + act_timeout_ms: float | Omit = omit, + browser: session_start_params.Browser | Omit = omit, + browserbase_session_create_params: session_start_params.BrowserbaseSessionCreateParams | Omit = omit, + browserbase_session_id: str | Omit = omit, + debug_dom: bool | Omit = omit, + dom_settle_timeout_ms: float | Omit = omit, + experimental: bool | Omit = omit, + self_heal: bool | Omit = omit, + system_prompt: str | Omit = omit, + verbose: Literal[0, 1, 2] | Omit = omit, + wait_for_captcha_solves: bool | Omit = omit, + x_language: Literal["typescript", "python", "playground"] | Omit = omit, + x_sdk_version: str | Omit = omit, + x_sent_at: Union[str, datetime] | Omit = omit, + x_stream_response: Literal["true", "false"] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> object: + ) -> SessionStartResponse: """Creates a new browser session with the specified configuration. Returns a session ID used for all subsequent operations. Args: + model_name: Model name to use for AI operations + + act_timeout_ms: Timeout in ms for act operations + + browserbase_session_id: Existing Browserbase session ID to resume + + dom_settle_timeout_ms: Timeout in ms to wait for DOM to settle + + self_heal: Enable self-healing for failed actions + + system_prompt: Custom system prompt for AI operations + + verbose: Logging verbosity level (0=quiet, 1=normal, 2=debug) + + x_language: Client SDK language + + x_sdk_version: Version of the Stagehand SDK + + x_sent_at: ISO timestamp when request was sent + + x_stream_response: Whether to stream the response via SSE + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -726,20 +1097,36 @@ async def start( **strip_not_given( { "x-language": str(x_language) if is_given(x_language) else not_given, - "x-sdk-version": str(x_sdk_version) if is_given(x_sdk_version) else not_given, - "x-sent-at": str(x_sent_at) if is_given(x_sent_at) else not_given, + "x-sdk-version": x_sdk_version, + "x-sent-at": x_sent_at.isoformat() if is_given(x_sent_at) else not_given, "x-stream-response": str(x_stream_response) if is_given(x_stream_response) else not_given, } ), **(extra_headers or {}), } return await self._post( - "/sessions/start", - body=await async_maybe_transform(body, session_start_params.SessionStartParams), + "/v1/sessions/start", + body=await async_maybe_transform( + { + "model_name": model_name, + "act_timeout_ms": act_timeout_ms, + "browser": browser, + "browserbase_session_create_params": browserbase_session_create_params, + "browserbase_session_id": browserbase_session_id, + "debug_dom": debug_dom, + "dom_settle_timeout_ms": dom_settle_timeout_ms, + "experimental": experimental, + "self_heal": self_heal, + "system_prompt": system_prompt, + "verbose": verbose, + "wait_for_captcha_solves": wait_for_captcha_solves, + }, + session_start_params.SessionStartParams, + ), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=object, + cast_to=SessionStartResponse, ) @@ -753,8 +1140,8 @@ def __init__(self, sessions: SessionsResource) -> None: self.end = to_raw_response_wrapper( sessions.end, ) - self.execute_agent = to_raw_response_wrapper( - sessions.execute_agent, + self.execute = to_raw_response_wrapper( + sessions.execute, ) self.extract = to_raw_response_wrapper( sessions.extract, @@ -780,8 +1167,8 @@ def __init__(self, sessions: AsyncSessionsResource) -> None: self.end = async_to_raw_response_wrapper( sessions.end, ) - self.execute_agent = async_to_raw_response_wrapper( - sessions.execute_agent, + self.execute = async_to_raw_response_wrapper( + sessions.execute, ) self.extract = async_to_raw_response_wrapper( sessions.extract, @@ -807,8 +1194,8 @@ def __init__(self, sessions: SessionsResource) -> None: self.end = to_streamed_response_wrapper( sessions.end, ) - self.execute_agent = to_streamed_response_wrapper( - sessions.execute_agent, + self.execute = to_streamed_response_wrapper( + sessions.execute, ) self.extract = to_streamed_response_wrapper( sessions.extract, @@ -834,8 +1221,8 @@ def __init__(self, sessions: AsyncSessionsResource) -> None: self.end = async_to_streamed_response_wrapper( sessions.end, ) - self.execute_agent = async_to_streamed_response_wrapper( - sessions.execute_agent, + self.execute = async_to_streamed_response_wrapper( + sessions.execute, ) self.extract = async_to_streamed_response_wrapper( sessions.extract, diff --git a/src/stagehand/types/__init__.py b/src/stagehand/types/__init__.py index acf81383..68575911 100644 --- a/src/stagehand/types/__init__.py +++ b/src/stagehand/types/__init__.py @@ -2,9 +2,18 @@ from __future__ import annotations +from .action import Action as Action +from .model_config_param import ModelConfigParam as ModelConfigParam from .session_act_params import SessionActParams as SessionActParams +from .session_act_response import SessionActResponse as SessionActResponse +from .session_end_response import SessionEndResponse as SessionEndResponse from .session_start_params import SessionStartParams as SessionStartParams +from .session_execute_params import SessionExecuteParams as SessionExecuteParams from .session_extract_params import SessionExtractParams as SessionExtractParams from .session_observe_params import SessionObserveParams as SessionObserveParams +from .session_start_response import SessionStartResponse as SessionStartResponse from .session_navigate_params import SessionNavigateParams as SessionNavigateParams -from .session_execute_agent_params import SessionExecuteAgentParams as SessionExecuteAgentParams +from .session_execute_response import SessionExecuteResponse as SessionExecuteResponse +from .session_extract_response import SessionExtractResponse as SessionExtractResponse +from .session_observe_response import SessionObserveResponse as SessionObserveResponse +from .session_navigate_response import SessionNavigateResponse as SessionNavigateResponse diff --git a/src/stagehand/types/action.py b/src/stagehand/types/action.py new file mode 100644 index 00000000..02ca3ebc --- /dev/null +++ b/src/stagehand/types/action.py @@ -0,0 +1,23 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List, Optional + +from .._models import BaseModel + +__all__ = ["Action"] + + +class Action(BaseModel): + """Action object returned by observe and used by act""" + + description: str + """Human-readable description of the action""" + + selector: str + """CSS selector or XPath for the element""" + + arguments: Optional[List[str]] = None + """Arguments to pass to the method""" + + method: Optional[str] = None + """The method to execute (click, fill, etc.)""" diff --git a/src/stagehand/types/model_config_param.py b/src/stagehand/types/model_config_param.py new file mode 100644 index 00000000..5e897c89 --- /dev/null +++ b/src/stagehand/types/model_config_param.py @@ -0,0 +1,21 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Union +from typing_extensions import Required, Annotated, TypeAlias, TypedDict + +from .._utils import PropertyInfo + +__all__ = ["ModelConfigParam", "UnionMember1"] + + +class UnionMember1(TypedDict, total=False): + model_name: Required[Annotated[str, PropertyInfo(alias="modelName")]] + + api_key: Annotated[str, PropertyInfo(alias="apiKey")] + + base_url: Annotated[str, PropertyInfo(alias="baseURL")] + + +ModelConfigParam: TypeAlias = Union[str, UnionMember1] diff --git a/src/stagehand/types/session_act_params.py b/src/stagehand/types/session_act_params.py index 6d23c710..4840ebfd 100644 --- a/src/stagehand/types/session_act_params.py +++ b/src/stagehand/types/session_act_params.py @@ -2,20 +2,63 @@ from __future__ import annotations -from typing_extensions import Annotated, TypedDict +from typing import Dict, Union +from datetime import datetime +from typing_extensions import Literal, Required, Annotated, TypeAlias, TypedDict +from .._types import SequenceNotStr from .._utils import PropertyInfo +from .model_config_param import ModelConfigParam -__all__ = ["SessionActParams"] +__all__ = ["SessionActParams", "Input", "InputActionInput", "Options"] class SessionActParams(TypedDict, total=False): - body: object + input: Required[Input] + """Natural language instruction or Action object""" - x_language: Annotated[object, PropertyInfo(alias="x-language")] + frame_id: Annotated[str, PropertyInfo(alias="frameId")] + """Target frame ID for the action""" - x_sdk_version: Annotated[object, PropertyInfo(alias="x-sdk-version")] + options: Options - x_sent_at: Annotated[object, PropertyInfo(alias="x-sent-at")] + x_language: Annotated[Literal["typescript", "python", "playground"], PropertyInfo(alias="x-language")] + """Client SDK language""" - x_stream_response: Annotated[object, PropertyInfo(alias="x-stream-response")] + x_sdk_version: Annotated[str, PropertyInfo(alias="x-sdk-version")] + """Version of the Stagehand SDK""" + + x_sent_at: Annotated[Union[str, datetime], PropertyInfo(alias="x-sent-at", format="iso8601")] + """ISO timestamp when request was sent""" + + x_stream_response: Annotated[Literal["true", "false"], PropertyInfo(alias="x-stream-response")] + """Whether to stream the response via SSE""" + + +class InputActionInput(TypedDict, total=False): + """Action object returned by observe and used by act""" + + description: Required[str] + """Human-readable description of the action""" + + selector: Required[str] + """CSS selector or XPath for the element""" + + arguments: SequenceNotStr[str] + """Arguments to pass to the method""" + + method: str + """The method to execute (click, fill, etc.)""" + + +Input: TypeAlias = Union[str, InputActionInput] + + +class Options(TypedDict, total=False): + model: ModelConfigParam + + timeout: float + """Timeout in ms for the action""" + + variables: Dict[str, str] + """Variables to substitute in the action instruction""" diff --git a/src/stagehand/types/session_act_response.py b/src/stagehand/types/session_act_response.py new file mode 100644 index 00000000..c9ecd059 --- /dev/null +++ b/src/stagehand/types/session_act_response.py @@ -0,0 +1,38 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List, Optional +from typing_extensions import Literal + +from pydantic import Field as FieldInfo + +from .action import Action +from .._models import BaseModel + +__all__ = ["SessionActResponse", "Data", "DataResult"] + + +class DataResult(BaseModel): + action_description: str = FieldInfo(alias="actionDescription") + """Description of the action that was performed""" + + actions: List[Action] + """List of actions that were executed""" + + message: str + """Human-readable result message""" + + success: bool + """Whether the action completed successfully""" + + +class Data(BaseModel): + result: DataResult + + action_id: Optional[str] = FieldInfo(alias="actionId", default=None) + """Action ID for tracking""" + + +class SessionActResponse(BaseModel): + data: Data + + success: Literal[True] diff --git a/src/stagehand/types/session_end_response.py b/src/stagehand/types/session_end_response.py new file mode 100644 index 00000000..7e538b37 --- /dev/null +++ b/src/stagehand/types/session_end_response.py @@ -0,0 +1,11 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing_extensions import Literal + +from .._models import BaseModel + +__all__ = ["SessionEndResponse"] + + +class SessionEndResponse(BaseModel): + success: Literal[True] diff --git a/src/stagehand/types/session_execute_agent_params.py b/src/stagehand/types/session_execute_agent_params.py deleted file mode 100644 index 891e3908..00000000 --- a/src/stagehand/types/session_execute_agent_params.py +++ /dev/null @@ -1,21 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -from typing_extensions import Annotated, TypedDict - -from .._utils import PropertyInfo - -__all__ = ["SessionExecuteAgentParams"] - - -class SessionExecuteAgentParams(TypedDict, total=False): - body: object - - x_language: Annotated[object, PropertyInfo(alias="x-language")] - - x_sdk_version: Annotated[object, PropertyInfo(alias="x-sdk-version")] - - x_sent_at: Annotated[object, PropertyInfo(alias="x-sent-at")] - - x_stream_response: Annotated[object, PropertyInfo(alias="x-stream-response")] diff --git a/src/stagehand/types/session_execute_params.py b/src/stagehand/types/session_execute_params.py new file mode 100644 index 00000000..38fc1c3b --- /dev/null +++ b/src/stagehand/types/session_execute_params.py @@ -0,0 +1,54 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Union +from datetime import datetime +from typing_extensions import Literal, Required, Annotated, TypedDict + +from .._utils import PropertyInfo +from .model_config_param import ModelConfigParam + +__all__ = ["SessionExecuteParams", "AgentConfig", "ExecuteOptions"] + + +class SessionExecuteParams(TypedDict, total=False): + agent_config: Required[Annotated[AgentConfig, PropertyInfo(alias="agentConfig")]] + + execute_options: Required[Annotated[ExecuteOptions, PropertyInfo(alias="executeOptions")]] + + frame_id: Annotated[str, PropertyInfo(alias="frameId")] + """Target frame ID for the agent""" + + x_language: Annotated[Literal["typescript", "python", "playground"], PropertyInfo(alias="x-language")] + """Client SDK language""" + + x_sdk_version: Annotated[str, PropertyInfo(alias="x-sdk-version")] + """Version of the Stagehand SDK""" + + x_sent_at: Annotated[Union[str, datetime], PropertyInfo(alias="x-sent-at", format="iso8601")] + """ISO timestamp when request was sent""" + + x_stream_response: Annotated[Literal["true", "false"], PropertyInfo(alias="x-stream-response")] + """Whether to stream the response via SSE""" + + +class AgentConfig(TypedDict, total=False): + cua: bool + """Enable Computer Use Agent mode""" + + model: ModelConfigParam + + system_prompt: Annotated[str, PropertyInfo(alias="systemPrompt")] + """Custom system prompt for the agent""" + + +class ExecuteOptions(TypedDict, total=False): + instruction: Required[str] + """Natural language instruction for the agent""" + + highlight_cursor: Annotated[bool, PropertyInfo(alias="highlightCursor")] + """Whether to visually highlight the cursor during execution""" + + max_steps: Annotated[float, PropertyInfo(alias="maxSteps")] + """Maximum number of steps the agent can take""" diff --git a/src/stagehand/types/session_execute_response.py b/src/stagehand/types/session_execute_response.py new file mode 100644 index 00000000..dd750071 --- /dev/null +++ b/src/stagehand/types/session_execute_response.py @@ -0,0 +1,82 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import TYPE_CHECKING, Dict, List, Optional +from typing_extensions import Literal + +from pydantic import Field as FieldInfo + +from .._models import BaseModel + +__all__ = ["SessionExecuteResponse", "Data", "DataResult", "DataResultAction", "DataResultUsage"] + + +class DataResultAction(BaseModel): + type: str + """Type of action taken""" + + action: Optional[str] = None + + instruction: Optional[str] = None + + page_text: Optional[str] = FieldInfo(alias="pageText", default=None) + + page_url: Optional[str] = FieldInfo(alias="pageUrl", default=None) + + reasoning: Optional[str] = None + """Agent's reasoning for taking this action""" + + task_completed: Optional[bool] = FieldInfo(alias="taskCompleted", default=None) + + time_ms: Optional[float] = FieldInfo(alias="timeMs", default=None) + """Time taken for this action in ms""" + + if TYPE_CHECKING: + # Some versions of Pydantic <2.8.0 have a bug and don’t allow assigning a + # value to this field, so for compatibility we avoid doing it at runtime. + __pydantic_extra__: Dict[str, object] = FieldInfo(init=False) # pyright: ignore[reportIncompatibleVariableOverride] + + # Stub to indicate that arbitrary properties are accepted. + # To access properties that are not valid identifiers you can use `getattr`, e.g. + # `getattr(obj, '$type')` + def __getattr__(self, attr: str) -> object: ... + else: + __pydantic_extra__: Dict[str, object] + + +class DataResultUsage(BaseModel): + inference_time_ms: float + + input_tokens: float + + output_tokens: float + + cached_input_tokens: Optional[float] = None + + reasoning_tokens: Optional[float] = None + + +class DataResult(BaseModel): + actions: List[DataResultAction] + + completed: bool + """Whether the agent finished its task""" + + message: str + """Summary of what the agent accomplished""" + + success: bool + """Whether the agent completed successfully""" + + metadata: Optional[Dict[str, object]] = None + + usage: Optional[DataResultUsage] = None + + +class Data(BaseModel): + result: DataResult + + +class SessionExecuteResponse(BaseModel): + data: Data + + success: Literal[True] diff --git a/src/stagehand/types/session_extract_params.py b/src/stagehand/types/session_extract_params.py index 4c6be4e5..22fb4863 100644 --- a/src/stagehand/types/session_extract_params.py +++ b/src/stagehand/types/session_extract_params.py @@ -2,20 +2,46 @@ from __future__ import annotations -from typing_extensions import Annotated, TypedDict +from typing import Dict, Union +from datetime import datetime +from typing_extensions import Literal, Annotated, TypedDict from .._utils import PropertyInfo +from .model_config_param import ModelConfigParam -__all__ = ["SessionExtractParams"] +__all__ = ["SessionExtractParams", "Options"] class SessionExtractParams(TypedDict, total=False): - body: object + frame_id: Annotated[str, PropertyInfo(alias="frameId")] + """Target frame ID for the extraction""" - x_language: Annotated[object, PropertyInfo(alias="x-language")] + instruction: str + """Natural language instruction for what to extract""" - x_sdk_version: Annotated[object, PropertyInfo(alias="x-sdk-version")] + options: Options - x_sent_at: Annotated[object, PropertyInfo(alias="x-sent-at")] + schema: Dict[str, object] + """JSON Schema defining the structure of data to extract""" - x_stream_response: Annotated[object, PropertyInfo(alias="x-stream-response")] + x_language: Annotated[Literal["typescript", "python", "playground"], PropertyInfo(alias="x-language")] + """Client SDK language""" + + x_sdk_version: Annotated[str, PropertyInfo(alias="x-sdk-version")] + """Version of the Stagehand SDK""" + + x_sent_at: Annotated[Union[str, datetime], PropertyInfo(alias="x-sent-at", format="iso8601")] + """ISO timestamp when request was sent""" + + x_stream_response: Annotated[Literal["true", "false"], PropertyInfo(alias="x-stream-response")] + """Whether to stream the response via SSE""" + + +class Options(TypedDict, total=False): + model: ModelConfigParam + + selector: str + """CSS selector to scope extraction to a specific element""" + + timeout: float + """Timeout in ms for the extraction""" diff --git a/src/stagehand/types/session_extract_response.py b/src/stagehand/types/session_extract_response.py new file mode 100644 index 00000000..623cd5eb --- /dev/null +++ b/src/stagehand/types/session_extract_response.py @@ -0,0 +1,24 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional +from typing_extensions import Literal + +from pydantic import Field as FieldInfo + +from .._models import BaseModel + +__all__ = ["SessionExtractResponse", "Data"] + + +class Data(BaseModel): + result: object + """Extracted data matching the requested schema""" + + action_id: Optional[str] = FieldInfo(alias="actionId", default=None) + """Action ID for tracking""" + + +class SessionExtractResponse(BaseModel): + data: Data + + success: Literal[True] diff --git a/src/stagehand/types/session_navigate_params.py b/src/stagehand/types/session_navigate_params.py index 5ba72ba2..15205f06 100644 --- a/src/stagehand/types/session_navigate_params.py +++ b/src/stagehand/types/session_navigate_params.py @@ -2,20 +2,43 @@ from __future__ import annotations -from typing_extensions import Annotated, TypedDict +from typing import Union +from datetime import datetime +from typing_extensions import Literal, Required, Annotated, TypedDict from .._utils import PropertyInfo -__all__ = ["SessionNavigateParams"] +__all__ = ["SessionNavigateParams", "Options"] class SessionNavigateParams(TypedDict, total=False): - body: object + url: Required[str] + """URL to navigate to""" - x_language: Annotated[object, PropertyInfo(alias="x-language")] + frame_id: Annotated[str, PropertyInfo(alias="frameId")] + """Target frame ID for the navigation""" - x_sdk_version: Annotated[object, PropertyInfo(alias="x-sdk-version")] + options: Options - x_sent_at: Annotated[object, PropertyInfo(alias="x-sent-at")] + x_language: Annotated[Literal["typescript", "python", "playground"], PropertyInfo(alias="x-language")] + """Client SDK language""" - x_stream_response: Annotated[object, PropertyInfo(alias="x-stream-response")] + x_sdk_version: Annotated[str, PropertyInfo(alias="x-sdk-version")] + """Version of the Stagehand SDK""" + + x_sent_at: Annotated[Union[str, datetime], PropertyInfo(alias="x-sent-at", format="iso8601")] + """ISO timestamp when request was sent""" + + x_stream_response: Annotated[Literal["true", "false"], PropertyInfo(alias="x-stream-response")] + """Whether to stream the response via SSE""" + + +class Options(TypedDict, total=False): + referer: str + """Referer header to send with the request""" + + timeout: float + """Timeout in ms for the navigation""" + + wait_until: Annotated[Literal["load", "domcontentloaded", "networkidle"], PropertyInfo(alias="waitUntil")] + """When to consider navigation complete""" diff --git a/src/stagehand/types/session_navigate_response.py b/src/stagehand/types/session_navigate_response.py new file mode 100644 index 00000000..f8667686 --- /dev/null +++ b/src/stagehand/types/session_navigate_response.py @@ -0,0 +1,24 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional +from typing_extensions import Literal + +from pydantic import Field as FieldInfo + +from .._models import BaseModel + +__all__ = ["SessionNavigateResponse", "Data"] + + +class Data(BaseModel): + result: object + """Navigation response (Playwright Response object or null)""" + + action_id: Optional[str] = FieldInfo(alias="actionId", default=None) + """Action ID for tracking""" + + +class SessionNavigateResponse(BaseModel): + data: Data + + success: Literal[True] diff --git a/src/stagehand/types/session_observe_params.py b/src/stagehand/types/session_observe_params.py index 82e9e560..b915cfb8 100644 --- a/src/stagehand/types/session_observe_params.py +++ b/src/stagehand/types/session_observe_params.py @@ -2,20 +2,43 @@ from __future__ import annotations -from typing_extensions import Annotated, TypedDict +from typing import Union +from datetime import datetime +from typing_extensions import Literal, Annotated, TypedDict from .._utils import PropertyInfo +from .model_config_param import ModelConfigParam -__all__ = ["SessionObserveParams"] +__all__ = ["SessionObserveParams", "Options"] class SessionObserveParams(TypedDict, total=False): - body: object + frame_id: Annotated[str, PropertyInfo(alias="frameId")] + """Target frame ID for the observation""" - x_language: Annotated[object, PropertyInfo(alias="x-language")] + instruction: str + """Natural language instruction for what actions to find""" - x_sdk_version: Annotated[object, PropertyInfo(alias="x-sdk-version")] + options: Options - x_sent_at: Annotated[object, PropertyInfo(alias="x-sent-at")] + x_language: Annotated[Literal["typescript", "python", "playground"], PropertyInfo(alias="x-language")] + """Client SDK language""" - x_stream_response: Annotated[object, PropertyInfo(alias="x-stream-response")] + x_sdk_version: Annotated[str, PropertyInfo(alias="x-sdk-version")] + """Version of the Stagehand SDK""" + + x_sent_at: Annotated[Union[str, datetime], PropertyInfo(alias="x-sent-at", format="iso8601")] + """ISO timestamp when request was sent""" + + x_stream_response: Annotated[Literal["true", "false"], PropertyInfo(alias="x-stream-response")] + """Whether to stream the response via SSE""" + + +class Options(TypedDict, total=False): + model: ModelConfigParam + + selector: str + """CSS selector to scope observation to a specific element""" + + timeout: float + """Timeout in ms for the observation""" diff --git a/src/stagehand/types/session_observe_response.py b/src/stagehand/types/session_observe_response.py new file mode 100644 index 00000000..32d31c76 --- /dev/null +++ b/src/stagehand/types/session_observe_response.py @@ -0,0 +1,24 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List, Optional +from typing_extensions import Literal + +from pydantic import Field as FieldInfo + +from .action import Action +from .._models import BaseModel + +__all__ = ["SessionObserveResponse", "Data"] + + +class Data(BaseModel): + result: List[Action] + + action_id: Optional[str] = FieldInfo(alias="actionId", default=None) + """Action ID for tracking""" + + +class SessionObserveResponse(BaseModel): + data: Data + + success: Literal[True] diff --git a/src/stagehand/types/session_start_params.py b/src/stagehand/types/session_start_params.py index 790d685e..f73ce65a 100644 --- a/src/stagehand/types/session_start_params.py +++ b/src/stagehand/types/session_start_params.py @@ -2,20 +2,248 @@ from __future__ import annotations -from typing_extensions import Annotated, TypedDict +from typing import Dict, List, Union, Iterable +from datetime import datetime +from typing_extensions import Literal, Required, Annotated, TypeAlias, TypedDict +from .._types import SequenceNotStr from .._utils import PropertyInfo -__all__ = ["SessionStartParams"] +__all__ = [ + "SessionStartParams", + "Browser", + "BrowserLaunchOptions", + "BrowserLaunchOptionsProxy", + "BrowserLaunchOptionsViewport", + "BrowserbaseSessionCreateParams", + "BrowserbaseSessionCreateParamsBrowserSettings", + "BrowserbaseSessionCreateParamsBrowserSettingsContext", + "BrowserbaseSessionCreateParamsBrowserSettingsFingerprint", + "BrowserbaseSessionCreateParamsBrowserSettingsFingerprintScreen", + "BrowserbaseSessionCreateParamsBrowserSettingsViewport", + "BrowserbaseSessionCreateParamsProxiesUnionMember1", + "BrowserbaseSessionCreateParamsProxiesUnionMember1BrowserbaseProxyConfig", + "BrowserbaseSessionCreateParamsProxiesUnionMember1BrowserbaseProxyConfigGeolocation", + "BrowserbaseSessionCreateParamsProxiesUnionMember1ExternalProxyConfig", +] class SessionStartParams(TypedDict, total=False): - body: object + model_name: Required[Annotated[str, PropertyInfo(alias="modelName")]] + """Model name to use for AI operations""" - x_language: Annotated[object, PropertyInfo(alias="x-language")] + act_timeout_ms: Annotated[float, PropertyInfo(alias="actTimeoutMs")] + """Timeout in ms for act operations""" - x_sdk_version: Annotated[object, PropertyInfo(alias="x-sdk-version")] + browser: Browser - x_sent_at: Annotated[object, PropertyInfo(alias="x-sent-at")] + browserbase_session_create_params: Annotated[ + BrowserbaseSessionCreateParams, PropertyInfo(alias="browserbaseSessionCreateParams") + ] - x_stream_response: Annotated[object, PropertyInfo(alias="x-stream-response")] + browserbase_session_id: Annotated[str, PropertyInfo(alias="browserbaseSessionID")] + """Existing Browserbase session ID to resume""" + + debug_dom: Annotated[bool, PropertyInfo(alias="debugDom")] + + dom_settle_timeout_ms: Annotated[float, PropertyInfo(alias="domSettleTimeoutMs")] + """Timeout in ms to wait for DOM to settle""" + + experimental: bool + + self_heal: Annotated[bool, PropertyInfo(alias="selfHeal")] + """Enable self-healing for failed actions""" + + system_prompt: Annotated[str, PropertyInfo(alias="systemPrompt")] + """Custom system prompt for AI operations""" + + verbose: Literal[0, 1, 2] + """Logging verbosity level (0=quiet, 1=normal, 2=debug)""" + + wait_for_captcha_solves: Annotated[bool, PropertyInfo(alias="waitForCaptchaSolves")] + + x_language: Annotated[Literal["typescript", "python", "playground"], PropertyInfo(alias="x-language")] + """Client SDK language""" + + x_sdk_version: Annotated[str, PropertyInfo(alias="x-sdk-version")] + """Version of the Stagehand SDK""" + + x_sent_at: Annotated[Union[str, datetime], PropertyInfo(alias="x-sent-at", format="iso8601")] + """ISO timestamp when request was sent""" + + x_stream_response: Annotated[Literal["true", "false"], PropertyInfo(alias="x-stream-response")] + """Whether to stream the response via SSE""" + + +class BrowserLaunchOptionsProxy(TypedDict, total=False): + server: Required[str] + + bypass: str + + password: str + + username: str + + +class BrowserLaunchOptionsViewport(TypedDict, total=False): + height: Required[float] + + width: Required[float] + + +class BrowserLaunchOptions(TypedDict, total=False): + accept_downloads: Annotated[bool, PropertyInfo(alias="acceptDownloads")] + + args: SequenceNotStr[str] + + cdp_url: Annotated[str, PropertyInfo(alias="cdpUrl")] + + chromium_sandbox: Annotated[bool, PropertyInfo(alias="chromiumSandbox")] + + connect_timeout_ms: Annotated[float, PropertyInfo(alias="connectTimeoutMs")] + + device_scale_factor: Annotated[float, PropertyInfo(alias="deviceScaleFactor")] + + devtools: bool + + downloads_path: Annotated[str, PropertyInfo(alias="downloadsPath")] + + executable_path: Annotated[str, PropertyInfo(alias="executablePath")] + + has_touch: Annotated[bool, PropertyInfo(alias="hasTouch")] + + headless: bool + + ignore_default_args: Annotated[Union[bool, SequenceNotStr[str]], PropertyInfo(alias="ignoreDefaultArgs")] + + ignore_https_errors: Annotated[bool, PropertyInfo(alias="ignoreHTTPSErrors")] + + locale: str + + preserve_user_data_dir: Annotated[bool, PropertyInfo(alias="preserveUserDataDir")] + + proxy: BrowserLaunchOptionsProxy + + user_data_dir: Annotated[str, PropertyInfo(alias="userDataDir")] + + viewport: BrowserLaunchOptionsViewport + + +class Browser(TypedDict, total=False): + cdp_url: Annotated[str, PropertyInfo(alias="cdpUrl")] + """Chrome DevTools Protocol URL for connecting to existing browser""" + + launch_options: Annotated[BrowserLaunchOptions, PropertyInfo(alias="launchOptions")] + + type: Literal["local", "browserbase"] + """Browser type to use""" + + +class BrowserbaseSessionCreateParamsBrowserSettingsContext(TypedDict, total=False): + id: Required[str] + + persist: bool + + +class BrowserbaseSessionCreateParamsBrowserSettingsFingerprintScreen(TypedDict, total=False): + max_height: Annotated[float, PropertyInfo(alias="maxHeight")] + + max_width: Annotated[float, PropertyInfo(alias="maxWidth")] + + min_height: Annotated[float, PropertyInfo(alias="minHeight")] + + min_width: Annotated[float, PropertyInfo(alias="minWidth")] + + +class BrowserbaseSessionCreateParamsBrowserSettingsFingerprint(TypedDict, total=False): + browsers: List[Literal["chrome", "edge", "firefox", "safari"]] + + devices: List[Literal["desktop", "mobile"]] + + http_version: Annotated[Literal["1", "2"], PropertyInfo(alias="httpVersion")] + + locales: SequenceNotStr[str] + + operating_systems: Annotated[ + List[Literal["android", "ios", "linux", "macos", "windows"]], PropertyInfo(alias="operatingSystems") + ] + + screen: BrowserbaseSessionCreateParamsBrowserSettingsFingerprintScreen + + +class BrowserbaseSessionCreateParamsBrowserSettingsViewport(TypedDict, total=False): + height: float + + width: float + + +class BrowserbaseSessionCreateParamsBrowserSettings(TypedDict, total=False): + advanced_stealth: Annotated[bool, PropertyInfo(alias="advancedStealth")] + + block_ads: Annotated[bool, PropertyInfo(alias="blockAds")] + + context: BrowserbaseSessionCreateParamsBrowserSettingsContext + + extension_id: Annotated[str, PropertyInfo(alias="extensionId")] + + fingerprint: BrowserbaseSessionCreateParamsBrowserSettingsFingerprint + + log_session: Annotated[bool, PropertyInfo(alias="logSession")] + + record_session: Annotated[bool, PropertyInfo(alias="recordSession")] + + solve_captchas: Annotated[bool, PropertyInfo(alias="solveCaptchas")] + + viewport: BrowserbaseSessionCreateParamsBrowserSettingsViewport + + +class BrowserbaseSessionCreateParamsProxiesUnionMember1BrowserbaseProxyConfigGeolocation(TypedDict, total=False): + country: Required[str] + + city: str + + state: str + + +class BrowserbaseSessionCreateParamsProxiesUnionMember1BrowserbaseProxyConfig(TypedDict, total=False): + type: Required[Literal["browserbase"]] + + domain_pattern: Annotated[str, PropertyInfo(alias="domainPattern")] + + geolocation: BrowserbaseSessionCreateParamsProxiesUnionMember1BrowserbaseProxyConfigGeolocation + + +class BrowserbaseSessionCreateParamsProxiesUnionMember1ExternalProxyConfig(TypedDict, total=False): + server: Required[str] + + type: Required[Literal["external"]] + + domain_pattern: Annotated[str, PropertyInfo(alias="domainPattern")] + + password: str + + username: str + + +BrowserbaseSessionCreateParamsProxiesUnionMember1: TypeAlias = Union[ + BrowserbaseSessionCreateParamsProxiesUnionMember1BrowserbaseProxyConfig, + BrowserbaseSessionCreateParamsProxiesUnionMember1ExternalProxyConfig, +] + + +class BrowserbaseSessionCreateParams(TypedDict, total=False): + browser_settings: Annotated[BrowserbaseSessionCreateParamsBrowserSettings, PropertyInfo(alias="browserSettings")] + + extension_id: Annotated[str, PropertyInfo(alias="extensionId")] + + keep_alive: Annotated[bool, PropertyInfo(alias="keepAlive")] + + project_id: Annotated[str, PropertyInfo(alias="projectId")] + + proxies: Union[bool, Iterable[BrowserbaseSessionCreateParamsProxiesUnionMember1]] + + region: Literal["us-west-2", "us-east-1", "eu-central-1", "ap-southeast-1"] + + timeout: float + + user_metadata: Annotated[Dict[str, object], PropertyInfo(alias="userMetadata")] diff --git a/src/stagehand/types/session_start_response.py b/src/stagehand/types/session_start_response.py new file mode 100644 index 00000000..c6715b1d --- /dev/null +++ b/src/stagehand/types/session_start_response.py @@ -0,0 +1,22 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing_extensions import Literal + +from pydantic import Field as FieldInfo + +from .._models import BaseModel + +__all__ = ["SessionStartResponse", "Data"] + + +class Data(BaseModel): + available: bool + + session_id: str = FieldInfo(alias="sessionId") + """Unique session identifier""" + + +class SessionStartResponse(BaseModel): + data: Data + + success: Literal[True] diff --git a/tests/api_resources/test_sessions.py b/tests/api_resources/test_sessions.py index 8e1ba863..29e86eb2 100644 --- a/tests/api_resources/test_sessions.py +++ b/tests/api_resources/test_sessions.py @@ -9,6 +9,16 @@ from stagehand import Stagehand, AsyncStagehand from tests.utils import assert_matches_type +from stagehand.types import ( + SessionActResponse, + SessionEndResponse, + SessionStartResponse, + SessionExecuteResponse, + SessionExtractResponse, + SessionObserveResponse, + SessionNavigateResponse, +) +from stagehand._utils import parse_datetime base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") @@ -20,320 +30,512 @@ class TestSessions: @parametrize def test_method_act(self, client: Stagehand) -> None: session = client.sessions.act( - id={}, + id="c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123", + input="Click the login button", ) - assert_matches_type(object, session, path=["response"]) + assert_matches_type(SessionActResponse, session, path=["response"]) @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_method_act_with_all_params(self, client: Stagehand) -> None: session = client.sessions.act( - id={}, - body={}, - x_language={}, - x_sdk_version={}, - x_sent_at={}, - x_stream_response={}, + id="c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123", + input="Click the login button", + frame_id="frameId", + options={ + "model": "string", + "timeout": 30000, + "variables": {"username": "john_doe"}, + }, + x_language="typescript", + x_sdk_version="3.0.6", + x_sent_at=parse_datetime("2025-01-15T10:30:00.000Z"), + x_stream_response="true", ) - assert_matches_type(object, session, path=["response"]) + assert_matches_type(SessionActResponse, session, path=["response"]) @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_raw_response_act(self, client: Stagehand) -> None: response = client.sessions.with_raw_response.act( - id={}, + id="c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123", + input="Click the login button", ) assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" session = response.parse() - assert_matches_type(object, session, path=["response"]) + assert_matches_type(SessionActResponse, session, path=["response"]) @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_streaming_response_act(self, client: Stagehand) -> None: with client.sessions.with_streaming_response.act( - id={}, + id="c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123", + input="Click the login button", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" session = response.parse() - assert_matches_type(object, session, path=["response"]) + assert_matches_type(SessionActResponse, session, path=["response"]) assert cast(Any, response.is_closed) is True + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_path_params_act(self, client: Stagehand) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.sessions.with_raw_response.act( + id="", + input="Click the login button", + ) + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_method_end(self, client: Stagehand) -> None: session = client.sessions.end( - id={}, + id="c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123", ) - assert_matches_type(object, session, path=["response"]) + assert_matches_type(SessionEndResponse, session, path=["response"]) @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_method_end_with_all_params(self, client: Stagehand) -> None: session = client.sessions.end( - id={}, - x_language={}, - x_sdk_version={}, - x_sent_at={}, - x_stream_response={}, + id="c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123", + x_language="typescript", + x_sdk_version="3.0.6", + x_sent_at=parse_datetime("2025-01-15T10:30:00.000Z"), + x_stream_response="true", ) - assert_matches_type(object, session, path=["response"]) + assert_matches_type(SessionEndResponse, session, path=["response"]) @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_raw_response_end(self, client: Stagehand) -> None: response = client.sessions.with_raw_response.end( - id={}, + id="c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123", ) assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" session = response.parse() - assert_matches_type(object, session, path=["response"]) + assert_matches_type(SessionEndResponse, session, path=["response"]) @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_streaming_response_end(self, client: Stagehand) -> None: with client.sessions.with_streaming_response.end( - id={}, + id="c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" session = response.parse() - assert_matches_type(object, session, path=["response"]) + assert_matches_type(SessionEndResponse, session, path=["response"]) assert cast(Any, response.is_closed) is True @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - def test_method_execute_agent(self, client: Stagehand) -> None: - session = client.sessions.execute_agent( - id={}, + def test_path_params_end(self, client: Stagehand) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.sessions.with_raw_response.end( + id="", + ) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_execute(self, client: Stagehand) -> None: + session = client.sessions.execute( + id="c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123", + agent_config={}, + execute_options={ + "instruction": "Log in with username 'demo' and password 'test123', then navigate to settings" + }, ) - assert_matches_type(object, session, path=["response"]) + assert_matches_type(SessionExecuteResponse, session, path=["response"]) @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - def test_method_execute_agent_with_all_params(self, client: Stagehand) -> None: - session = client.sessions.execute_agent( - id={}, - body={}, - x_language={}, - x_sdk_version={}, - x_sent_at={}, - x_stream_response={}, + def test_method_execute_with_all_params(self, client: Stagehand) -> None: + session = client.sessions.execute( + id="c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123", + agent_config={ + "cua": True, + "model": "string", + "system_prompt": "systemPrompt", + }, + execute_options={ + "instruction": "Log in with username 'demo' and password 'test123', then navigate to settings", + "highlight_cursor": True, + "max_steps": 20, + }, + frame_id="frameId", + x_language="typescript", + x_sdk_version="3.0.6", + x_sent_at=parse_datetime("2025-01-15T10:30:00.000Z"), + x_stream_response="true", ) - assert_matches_type(object, session, path=["response"]) + assert_matches_type(SessionExecuteResponse, session, path=["response"]) @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - def test_raw_response_execute_agent(self, client: Stagehand) -> None: - response = client.sessions.with_raw_response.execute_agent( - id={}, + def test_raw_response_execute(self, client: Stagehand) -> None: + response = client.sessions.with_raw_response.execute( + id="c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123", + agent_config={}, + execute_options={ + "instruction": "Log in with username 'demo' and password 'test123', then navigate to settings" + }, ) assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" session = response.parse() - assert_matches_type(object, session, path=["response"]) + assert_matches_type(SessionExecuteResponse, session, path=["response"]) @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - def test_streaming_response_execute_agent(self, client: Stagehand) -> None: - with client.sessions.with_streaming_response.execute_agent( - id={}, + def test_streaming_response_execute(self, client: Stagehand) -> None: + with client.sessions.with_streaming_response.execute( + id="c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123", + agent_config={}, + execute_options={ + "instruction": "Log in with username 'demo' and password 'test123', then navigate to settings" + }, ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" session = response.parse() - assert_matches_type(object, session, path=["response"]) + assert_matches_type(SessionExecuteResponse, session, path=["response"]) assert cast(Any, response.is_closed) is True + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_path_params_execute(self, client: Stagehand) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.sessions.with_raw_response.execute( + id="", + agent_config={}, + execute_options={ + "instruction": "Log in with username 'demo' and password 'test123', then navigate to settings" + }, + ) + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_method_extract(self, client: Stagehand) -> None: session = client.sessions.extract( - id={}, + id="c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123", ) - assert_matches_type(object, session, path=["response"]) + assert_matches_type(SessionExtractResponse, session, path=["response"]) @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_method_extract_with_all_params(self, client: Stagehand) -> None: session = client.sessions.extract( - id={}, - body={}, - x_language={}, - x_sdk_version={}, - x_sent_at={}, - x_stream_response={}, + id="c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123", + frame_id="frameId", + instruction="Extract all product names and prices from the page", + options={ + "model": "string", + "selector": "#main-content", + "timeout": 30000, + }, + schema={"foo": "bar"}, + x_language="typescript", + x_sdk_version="3.0.6", + x_sent_at=parse_datetime("2025-01-15T10:30:00.000Z"), + x_stream_response="true", ) - assert_matches_type(object, session, path=["response"]) + assert_matches_type(SessionExtractResponse, session, path=["response"]) @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_raw_response_extract(self, client: Stagehand) -> None: response = client.sessions.with_raw_response.extract( - id={}, + id="c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123", ) assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" session = response.parse() - assert_matches_type(object, session, path=["response"]) + assert_matches_type(SessionExtractResponse, session, path=["response"]) @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_streaming_response_extract(self, client: Stagehand) -> None: with client.sessions.with_streaming_response.extract( - id={}, + id="c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" session = response.parse() - assert_matches_type(object, session, path=["response"]) + assert_matches_type(SessionExtractResponse, session, path=["response"]) assert cast(Any, response.is_closed) is True + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_path_params_extract(self, client: Stagehand) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.sessions.with_raw_response.extract( + id="", + ) + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_method_navigate(self, client: Stagehand) -> None: session = client.sessions.navigate( - id={}, + id="c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123", + url="https://example.com", ) - assert_matches_type(object, session, path=["response"]) + assert_matches_type(SessionNavigateResponse, session, path=["response"]) @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_method_navigate_with_all_params(self, client: Stagehand) -> None: session = client.sessions.navigate( - id={}, - body={}, - x_language={}, - x_sdk_version={}, - x_sent_at={}, - x_stream_response={}, + id="c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123", + url="https://example.com", + frame_id="frameId", + options={ + "referer": "referer", + "timeout": 30000, + "wait_until": "networkidle", + }, + x_language="typescript", + x_sdk_version="3.0.6", + x_sent_at=parse_datetime("2025-01-15T10:30:00.000Z"), + x_stream_response="true", ) - assert_matches_type(object, session, path=["response"]) + assert_matches_type(SessionNavigateResponse, session, path=["response"]) @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_raw_response_navigate(self, client: Stagehand) -> None: response = client.sessions.with_raw_response.navigate( - id={}, + id="c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123", + url="https://example.com", ) assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" session = response.parse() - assert_matches_type(object, session, path=["response"]) + assert_matches_type(SessionNavigateResponse, session, path=["response"]) @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_streaming_response_navigate(self, client: Stagehand) -> None: with client.sessions.with_streaming_response.navigate( - id={}, + id="c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123", + url="https://example.com", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" session = response.parse() - assert_matches_type(object, session, path=["response"]) + assert_matches_type(SessionNavigateResponse, session, path=["response"]) assert cast(Any, response.is_closed) is True + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_path_params_navigate(self, client: Stagehand) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.sessions.with_raw_response.navigate( + id="", + url="https://example.com", + ) + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_method_observe(self, client: Stagehand) -> None: session = client.sessions.observe( - id={}, + id="c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123", ) - assert_matches_type(object, session, path=["response"]) + assert_matches_type(SessionObserveResponse, session, path=["response"]) @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_method_observe_with_all_params(self, client: Stagehand) -> None: session = client.sessions.observe( - id={}, - body={}, - x_language={}, - x_sdk_version={}, - x_sent_at={}, - x_stream_response={}, + id="c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123", + frame_id="frameId", + instruction="Find all clickable navigation links", + options={ + "model": "string", + "selector": "nav", + "timeout": 30000, + }, + x_language="typescript", + x_sdk_version="3.0.6", + x_sent_at=parse_datetime("2025-01-15T10:30:00.000Z"), + x_stream_response="true", ) - assert_matches_type(object, session, path=["response"]) + assert_matches_type(SessionObserveResponse, session, path=["response"]) @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_raw_response_observe(self, client: Stagehand) -> None: response = client.sessions.with_raw_response.observe( - id={}, + id="c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123", ) assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" session = response.parse() - assert_matches_type(object, session, path=["response"]) + assert_matches_type(SessionObserveResponse, session, path=["response"]) @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_streaming_response_observe(self, client: Stagehand) -> None: with client.sessions.with_streaming_response.observe( - id={}, + id="c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" session = response.parse() - assert_matches_type(object, session, path=["response"]) + assert_matches_type(SessionObserveResponse, session, path=["response"]) assert cast(Any, response.is_closed) is True + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_path_params_observe(self, client: Stagehand) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.sessions.with_raw_response.observe( + id="", + ) + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_method_start(self, client: Stagehand) -> None: - session = client.sessions.start() - assert_matches_type(object, session, path=["response"]) + session = client.sessions.start( + model_name="gpt-4o", + ) + assert_matches_type(SessionStartResponse, session, path=["response"]) @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_method_start_with_all_params(self, client: Stagehand) -> None: session = client.sessions.start( - body={}, - x_language={}, - x_sdk_version={}, - x_sent_at={}, - x_stream_response={}, + model_name="gpt-4o", + act_timeout_ms=30000, + browser={ + "cdp_url": "ws://localhost:9222", + "launch_options": { + "accept_downloads": True, + "args": ["string"], + "cdp_url": "cdpUrl", + "chromium_sandbox": True, + "connect_timeout_ms": 0, + "device_scale_factor": 0, + "devtools": True, + "downloads_path": "downloadsPath", + "executable_path": "executablePath", + "has_touch": True, + "headless": True, + "ignore_default_args": True, + "ignore_https_errors": True, + "locale": "locale", + "preserve_user_data_dir": True, + "proxy": { + "server": "server", + "bypass": "bypass", + "password": "password", + "username": "username", + }, + "user_data_dir": "userDataDir", + "viewport": { + "height": 0, + "width": 0, + }, + }, + "type": "local", + }, + browserbase_session_create_params={ + "browser_settings": { + "advanced_stealth": True, + "block_ads": True, + "context": { + "id": "id", + "persist": True, + }, + "extension_id": "extensionId", + "fingerprint": { + "browsers": ["chrome"], + "devices": ["desktop"], + "http_version": "1", + "locales": ["string"], + "operating_systems": ["android"], + "screen": { + "max_height": 0, + "max_width": 0, + "min_height": 0, + "min_width": 0, + }, + }, + "log_session": True, + "record_session": True, + "solve_captchas": True, + "viewport": { + "height": 0, + "width": 0, + }, + }, + "extension_id": "extensionId", + "keep_alive": True, + "project_id": "projectId", + "proxies": True, + "region": "us-west-2", + "timeout": 0, + "user_metadata": {"foo": "bar"}, + }, + browserbase_session_id="browserbaseSessionID", + debug_dom=True, + dom_settle_timeout_ms=5000, + experimental=True, + self_heal=True, + system_prompt="systemPrompt", + verbose=1, + wait_for_captcha_solves=True, + x_language="typescript", + x_sdk_version="3.0.6", + x_sent_at=parse_datetime("2025-01-15T10:30:00.000Z"), + x_stream_response="true", ) - assert_matches_type(object, session, path=["response"]) + assert_matches_type(SessionStartResponse, session, path=["response"]) @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_raw_response_start(self, client: Stagehand) -> None: - response = client.sessions.with_raw_response.start() + response = client.sessions.with_raw_response.start( + model_name="gpt-4o", + ) assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" session = response.parse() - assert_matches_type(object, session, path=["response"]) + assert_matches_type(SessionStartResponse, session, path=["response"]) @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_streaming_response_start(self, client: Stagehand) -> None: - with client.sessions.with_streaming_response.start() as response: + with client.sessions.with_streaming_response.start( + model_name="gpt-4o", + ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" session = response.parse() - assert_matches_type(object, session, path=["response"]) + assert_matches_type(SessionStartResponse, session, path=["response"]) assert cast(Any, response.is_closed) is True @@ -347,319 +549,511 @@ class TestAsyncSessions: @parametrize async def test_method_act(self, async_client: AsyncStagehand) -> None: session = await async_client.sessions.act( - id={}, + id="c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123", + input="Click the login button", ) - assert_matches_type(object, session, path=["response"]) + assert_matches_type(SessionActResponse, session, path=["response"]) @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_method_act_with_all_params(self, async_client: AsyncStagehand) -> None: session = await async_client.sessions.act( - id={}, - body={}, - x_language={}, - x_sdk_version={}, - x_sent_at={}, - x_stream_response={}, + id="c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123", + input="Click the login button", + frame_id="frameId", + options={ + "model": "string", + "timeout": 30000, + "variables": {"username": "john_doe"}, + }, + x_language="typescript", + x_sdk_version="3.0.6", + x_sent_at=parse_datetime("2025-01-15T10:30:00.000Z"), + x_stream_response="true", ) - assert_matches_type(object, session, path=["response"]) + assert_matches_type(SessionActResponse, session, path=["response"]) @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_raw_response_act(self, async_client: AsyncStagehand) -> None: response = await async_client.sessions.with_raw_response.act( - id={}, + id="c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123", + input="Click the login button", ) assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" session = await response.parse() - assert_matches_type(object, session, path=["response"]) + assert_matches_type(SessionActResponse, session, path=["response"]) @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_streaming_response_act(self, async_client: AsyncStagehand) -> None: async with async_client.sessions.with_streaming_response.act( - id={}, + id="c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123", + input="Click the login button", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" session = await response.parse() - assert_matches_type(object, session, path=["response"]) + assert_matches_type(SessionActResponse, session, path=["response"]) assert cast(Any, response.is_closed) is True + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_path_params_act(self, async_client: AsyncStagehand) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.sessions.with_raw_response.act( + id="", + input="Click the login button", + ) + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_method_end(self, async_client: AsyncStagehand) -> None: session = await async_client.sessions.end( - id={}, + id="c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123", ) - assert_matches_type(object, session, path=["response"]) + assert_matches_type(SessionEndResponse, session, path=["response"]) @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_method_end_with_all_params(self, async_client: AsyncStagehand) -> None: session = await async_client.sessions.end( - id={}, - x_language={}, - x_sdk_version={}, - x_sent_at={}, - x_stream_response={}, + id="c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123", + x_language="typescript", + x_sdk_version="3.0.6", + x_sent_at=parse_datetime("2025-01-15T10:30:00.000Z"), + x_stream_response="true", ) - assert_matches_type(object, session, path=["response"]) + assert_matches_type(SessionEndResponse, session, path=["response"]) @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_raw_response_end(self, async_client: AsyncStagehand) -> None: response = await async_client.sessions.with_raw_response.end( - id={}, + id="c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123", ) assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" session = await response.parse() - assert_matches_type(object, session, path=["response"]) + assert_matches_type(SessionEndResponse, session, path=["response"]) @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_streaming_response_end(self, async_client: AsyncStagehand) -> None: async with async_client.sessions.with_streaming_response.end( - id={}, + id="c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" session = await response.parse() - assert_matches_type(object, session, path=["response"]) + assert_matches_type(SessionEndResponse, session, path=["response"]) assert cast(Any, response.is_closed) is True @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - async def test_method_execute_agent(self, async_client: AsyncStagehand) -> None: - session = await async_client.sessions.execute_agent( - id={}, + async def test_path_params_end(self, async_client: AsyncStagehand) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.sessions.with_raw_response.end( + id="", + ) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_method_execute(self, async_client: AsyncStagehand) -> None: + session = await async_client.sessions.execute( + id="c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123", + agent_config={}, + execute_options={ + "instruction": "Log in with username 'demo' and password 'test123', then navigate to settings" + }, ) - assert_matches_type(object, session, path=["response"]) + assert_matches_type(SessionExecuteResponse, session, path=["response"]) @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - async def test_method_execute_agent_with_all_params(self, async_client: AsyncStagehand) -> None: - session = await async_client.sessions.execute_agent( - id={}, - body={}, - x_language={}, - x_sdk_version={}, - x_sent_at={}, - x_stream_response={}, + async def test_method_execute_with_all_params(self, async_client: AsyncStagehand) -> None: + session = await async_client.sessions.execute( + id="c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123", + agent_config={ + "cua": True, + "model": "string", + "system_prompt": "systemPrompt", + }, + execute_options={ + "instruction": "Log in with username 'demo' and password 'test123', then navigate to settings", + "highlight_cursor": True, + "max_steps": 20, + }, + frame_id="frameId", + x_language="typescript", + x_sdk_version="3.0.6", + x_sent_at=parse_datetime("2025-01-15T10:30:00.000Z"), + x_stream_response="true", ) - assert_matches_type(object, session, path=["response"]) + assert_matches_type(SessionExecuteResponse, session, path=["response"]) @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - async def test_raw_response_execute_agent(self, async_client: AsyncStagehand) -> None: - response = await async_client.sessions.with_raw_response.execute_agent( - id={}, + async def test_raw_response_execute(self, async_client: AsyncStagehand) -> None: + response = await async_client.sessions.with_raw_response.execute( + id="c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123", + agent_config={}, + execute_options={ + "instruction": "Log in with username 'demo' and password 'test123', then navigate to settings" + }, ) assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" session = await response.parse() - assert_matches_type(object, session, path=["response"]) + assert_matches_type(SessionExecuteResponse, session, path=["response"]) @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - async def test_streaming_response_execute_agent(self, async_client: AsyncStagehand) -> None: - async with async_client.sessions.with_streaming_response.execute_agent( - id={}, + async def test_streaming_response_execute(self, async_client: AsyncStagehand) -> None: + async with async_client.sessions.with_streaming_response.execute( + id="c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123", + agent_config={}, + execute_options={ + "instruction": "Log in with username 'demo' and password 'test123', then navigate to settings" + }, ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" session = await response.parse() - assert_matches_type(object, session, path=["response"]) + assert_matches_type(SessionExecuteResponse, session, path=["response"]) assert cast(Any, response.is_closed) is True + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_path_params_execute(self, async_client: AsyncStagehand) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.sessions.with_raw_response.execute( + id="", + agent_config={}, + execute_options={ + "instruction": "Log in with username 'demo' and password 'test123', then navigate to settings" + }, + ) + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_method_extract(self, async_client: AsyncStagehand) -> None: session = await async_client.sessions.extract( - id={}, + id="c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123", ) - assert_matches_type(object, session, path=["response"]) + assert_matches_type(SessionExtractResponse, session, path=["response"]) @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_method_extract_with_all_params(self, async_client: AsyncStagehand) -> None: session = await async_client.sessions.extract( - id={}, - body={}, - x_language={}, - x_sdk_version={}, - x_sent_at={}, - x_stream_response={}, + id="c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123", + frame_id="frameId", + instruction="Extract all product names and prices from the page", + options={ + "model": "string", + "selector": "#main-content", + "timeout": 30000, + }, + schema={"foo": "bar"}, + x_language="typescript", + x_sdk_version="3.0.6", + x_sent_at=parse_datetime("2025-01-15T10:30:00.000Z"), + x_stream_response="true", ) - assert_matches_type(object, session, path=["response"]) + assert_matches_type(SessionExtractResponse, session, path=["response"]) @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_raw_response_extract(self, async_client: AsyncStagehand) -> None: response = await async_client.sessions.with_raw_response.extract( - id={}, + id="c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123", ) assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" session = await response.parse() - assert_matches_type(object, session, path=["response"]) + assert_matches_type(SessionExtractResponse, session, path=["response"]) @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_streaming_response_extract(self, async_client: AsyncStagehand) -> None: async with async_client.sessions.with_streaming_response.extract( - id={}, + id="c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" session = await response.parse() - assert_matches_type(object, session, path=["response"]) + assert_matches_type(SessionExtractResponse, session, path=["response"]) assert cast(Any, response.is_closed) is True + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_path_params_extract(self, async_client: AsyncStagehand) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.sessions.with_raw_response.extract( + id="", + ) + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_method_navigate(self, async_client: AsyncStagehand) -> None: session = await async_client.sessions.navigate( - id={}, + id="c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123", + url="https://example.com", ) - assert_matches_type(object, session, path=["response"]) + assert_matches_type(SessionNavigateResponse, session, path=["response"]) @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_method_navigate_with_all_params(self, async_client: AsyncStagehand) -> None: session = await async_client.sessions.navigate( - id={}, - body={}, - x_language={}, - x_sdk_version={}, - x_sent_at={}, - x_stream_response={}, + id="c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123", + url="https://example.com", + frame_id="frameId", + options={ + "referer": "referer", + "timeout": 30000, + "wait_until": "networkidle", + }, + x_language="typescript", + x_sdk_version="3.0.6", + x_sent_at=parse_datetime("2025-01-15T10:30:00.000Z"), + x_stream_response="true", ) - assert_matches_type(object, session, path=["response"]) + assert_matches_type(SessionNavigateResponse, session, path=["response"]) @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_raw_response_navigate(self, async_client: AsyncStagehand) -> None: response = await async_client.sessions.with_raw_response.navigate( - id={}, + id="c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123", + url="https://example.com", ) assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" session = await response.parse() - assert_matches_type(object, session, path=["response"]) + assert_matches_type(SessionNavigateResponse, session, path=["response"]) @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_streaming_response_navigate(self, async_client: AsyncStagehand) -> None: async with async_client.sessions.with_streaming_response.navigate( - id={}, + id="c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123", + url="https://example.com", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" session = await response.parse() - assert_matches_type(object, session, path=["response"]) + assert_matches_type(SessionNavigateResponse, session, path=["response"]) assert cast(Any, response.is_closed) is True + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_path_params_navigate(self, async_client: AsyncStagehand) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.sessions.with_raw_response.navigate( + id="", + url="https://example.com", + ) + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_method_observe(self, async_client: AsyncStagehand) -> None: session = await async_client.sessions.observe( - id={}, + id="c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123", ) - assert_matches_type(object, session, path=["response"]) + assert_matches_type(SessionObserveResponse, session, path=["response"]) @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_method_observe_with_all_params(self, async_client: AsyncStagehand) -> None: session = await async_client.sessions.observe( - id={}, - body={}, - x_language={}, - x_sdk_version={}, - x_sent_at={}, - x_stream_response={}, + id="c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123", + frame_id="frameId", + instruction="Find all clickable navigation links", + options={ + "model": "string", + "selector": "nav", + "timeout": 30000, + }, + x_language="typescript", + x_sdk_version="3.0.6", + x_sent_at=parse_datetime("2025-01-15T10:30:00.000Z"), + x_stream_response="true", ) - assert_matches_type(object, session, path=["response"]) + assert_matches_type(SessionObserveResponse, session, path=["response"]) @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_raw_response_observe(self, async_client: AsyncStagehand) -> None: response = await async_client.sessions.with_raw_response.observe( - id={}, + id="c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123", ) assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" session = await response.parse() - assert_matches_type(object, session, path=["response"]) + assert_matches_type(SessionObserveResponse, session, path=["response"]) @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_streaming_response_observe(self, async_client: AsyncStagehand) -> None: async with async_client.sessions.with_streaming_response.observe( - id={}, + id="c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" session = await response.parse() - assert_matches_type(object, session, path=["response"]) + assert_matches_type(SessionObserveResponse, session, path=["response"]) assert cast(Any, response.is_closed) is True + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_path_params_observe(self, async_client: AsyncStagehand) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.sessions.with_raw_response.observe( + id="", + ) + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_method_start(self, async_client: AsyncStagehand) -> None: - session = await async_client.sessions.start() - assert_matches_type(object, session, path=["response"]) + session = await async_client.sessions.start( + model_name="gpt-4o", + ) + assert_matches_type(SessionStartResponse, session, path=["response"]) @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_method_start_with_all_params(self, async_client: AsyncStagehand) -> None: session = await async_client.sessions.start( - body={}, - x_language={}, - x_sdk_version={}, - x_sent_at={}, - x_stream_response={}, + model_name="gpt-4o", + act_timeout_ms=30000, + browser={ + "cdp_url": "ws://localhost:9222", + "launch_options": { + "accept_downloads": True, + "args": ["string"], + "cdp_url": "cdpUrl", + "chromium_sandbox": True, + "connect_timeout_ms": 0, + "device_scale_factor": 0, + "devtools": True, + "downloads_path": "downloadsPath", + "executable_path": "executablePath", + "has_touch": True, + "headless": True, + "ignore_default_args": True, + "ignore_https_errors": True, + "locale": "locale", + "preserve_user_data_dir": True, + "proxy": { + "server": "server", + "bypass": "bypass", + "password": "password", + "username": "username", + }, + "user_data_dir": "userDataDir", + "viewport": { + "height": 0, + "width": 0, + }, + }, + "type": "local", + }, + browserbase_session_create_params={ + "browser_settings": { + "advanced_stealth": True, + "block_ads": True, + "context": { + "id": "id", + "persist": True, + }, + "extension_id": "extensionId", + "fingerprint": { + "browsers": ["chrome"], + "devices": ["desktop"], + "http_version": "1", + "locales": ["string"], + "operating_systems": ["android"], + "screen": { + "max_height": 0, + "max_width": 0, + "min_height": 0, + "min_width": 0, + }, + }, + "log_session": True, + "record_session": True, + "solve_captchas": True, + "viewport": { + "height": 0, + "width": 0, + }, + }, + "extension_id": "extensionId", + "keep_alive": True, + "project_id": "projectId", + "proxies": True, + "region": "us-west-2", + "timeout": 0, + "user_metadata": {"foo": "bar"}, + }, + browserbase_session_id="browserbaseSessionID", + debug_dom=True, + dom_settle_timeout_ms=5000, + experimental=True, + self_heal=True, + system_prompt="systemPrompt", + verbose=1, + wait_for_captcha_solves=True, + x_language="typescript", + x_sdk_version="3.0.6", + x_sent_at=parse_datetime("2025-01-15T10:30:00.000Z"), + x_stream_response="true", ) - assert_matches_type(object, session, path=["response"]) + assert_matches_type(SessionStartResponse, session, path=["response"]) @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_raw_response_start(self, async_client: AsyncStagehand) -> None: - response = await async_client.sessions.with_raw_response.start() + response = await async_client.sessions.with_raw_response.start( + model_name="gpt-4o", + ) assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" session = await response.parse() - assert_matches_type(object, session, path=["response"]) + assert_matches_type(SessionStartResponse, session, path=["response"]) @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_streaming_response_start(self, async_client: AsyncStagehand) -> None: - async with async_client.sessions.with_streaming_response.start() as response: + async with async_client.sessions.with_streaming_response.start( + model_name="gpt-4o", + ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" session = await response.parse() - assert_matches_type(object, session, path=["response"]) + assert_matches_type(SessionStartResponse, session, path=["response"]) assert cast(Any, response.is_closed) is True diff --git a/tests/test_client.py b/tests/test_client.py index b1f716b0..df47cdaf 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -876,20 +876,26 @@ def test_parse_retry_after_header( @mock.patch("stagehand._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) @pytest.mark.respx(base_url=base_url) def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter, client: Stagehand) -> None: - respx_mock.post("/sessions/start").mock(side_effect=httpx.TimeoutException("Test timeout error")) + respx_mock.post("/v1/sessions/c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123/act").mock( + side_effect=httpx.TimeoutException("Test timeout error") + ) with pytest.raises(APITimeoutError): - client.sessions.with_streaming_response.start().__enter__() + client.sessions.with_streaming_response.act( + id="c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123", input="Click the login button" + ).__enter__() assert _get_open_connections(client) == 0 @mock.patch("stagehand._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) @pytest.mark.respx(base_url=base_url) def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter, client: Stagehand) -> None: - respx_mock.post("/sessions/start").mock(return_value=httpx.Response(500)) + respx_mock.post("/v1/sessions/c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123/act").mock(return_value=httpx.Response(500)) with pytest.raises(APIStatusError): - client.sessions.with_streaming_response.start().__enter__() + client.sessions.with_streaming_response.act( + id="c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123", input="Click the login button" + ).__enter__() assert _get_open_connections(client) == 0 @pytest.mark.parametrize("failures_before_success", [0, 2, 4]) @@ -916,9 +922,11 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: return httpx.Response(500) return httpx.Response(200) - respx_mock.post("/sessions/start").mock(side_effect=retry_handler) + respx_mock.post("/v1/sessions/c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123/act").mock(side_effect=retry_handler) - response = client.sessions.with_raw_response.start() + response = client.sessions.with_raw_response.act( + id="c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123", input="Click the login button" + ) assert response.retries_taken == failures_before_success assert int(response.http_request.headers.get("x-stainless-retry-count")) == failures_before_success @@ -940,9 +948,13 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: return httpx.Response(500) return httpx.Response(200) - respx_mock.post("/sessions/start").mock(side_effect=retry_handler) + respx_mock.post("/v1/sessions/c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123/act").mock(side_effect=retry_handler) - response = client.sessions.with_raw_response.start(extra_headers={"x-stainless-retry-count": Omit()}) + response = client.sessions.with_raw_response.act( + id="c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123", + input="Click the login button", + extra_headers={"x-stainless-retry-count": Omit()}, + ) assert len(response.http_request.headers.get_list("x-stainless-retry-count")) == 0 @@ -963,9 +975,13 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: return httpx.Response(500) return httpx.Response(200) - respx_mock.post("/sessions/start").mock(side_effect=retry_handler) + respx_mock.post("/v1/sessions/c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123/act").mock(side_effect=retry_handler) - response = client.sessions.with_raw_response.start(extra_headers={"x-stainless-retry-count": "42"}) + response = client.sessions.with_raw_response.act( + id="c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123", + input="Click the login button", + extra_headers={"x-stainless-retry-count": "42"}, + ) assert response.http_request.headers.get("x-stainless-retry-count") == "42" @@ -1840,10 +1856,14 @@ async def test_parse_retry_after_header( async def test_retrying_timeout_errors_doesnt_leak( self, respx_mock: MockRouter, async_client: AsyncStagehand ) -> None: - respx_mock.post("/sessions/start").mock(side_effect=httpx.TimeoutException("Test timeout error")) + respx_mock.post("/v1/sessions/c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123/act").mock( + side_effect=httpx.TimeoutException("Test timeout error") + ) with pytest.raises(APITimeoutError): - await async_client.sessions.with_streaming_response.start().__aenter__() + await async_client.sessions.with_streaming_response.act( + id="c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123", input="Click the login button" + ).__aenter__() assert _get_open_connections(async_client) == 0 @@ -1852,10 +1872,12 @@ async def test_retrying_timeout_errors_doesnt_leak( async def test_retrying_status_errors_doesnt_leak( self, respx_mock: MockRouter, async_client: AsyncStagehand ) -> None: - respx_mock.post("/sessions/start").mock(return_value=httpx.Response(500)) + respx_mock.post("/v1/sessions/c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123/act").mock(return_value=httpx.Response(500)) with pytest.raises(APIStatusError): - await async_client.sessions.with_streaming_response.start().__aenter__() + await async_client.sessions.with_streaming_response.act( + id="c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123", input="Click the login button" + ).__aenter__() assert _get_open_connections(async_client) == 0 @pytest.mark.parametrize("failures_before_success", [0, 2, 4]) @@ -1882,9 +1904,11 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: return httpx.Response(500) return httpx.Response(200) - respx_mock.post("/sessions/start").mock(side_effect=retry_handler) + respx_mock.post("/v1/sessions/c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123/act").mock(side_effect=retry_handler) - response = await client.sessions.with_raw_response.start() + response = await client.sessions.with_raw_response.act( + id="c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123", input="Click the login button" + ) assert response.retries_taken == failures_before_success assert int(response.http_request.headers.get("x-stainless-retry-count")) == failures_before_success @@ -1906,9 +1930,13 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: return httpx.Response(500) return httpx.Response(200) - respx_mock.post("/sessions/start").mock(side_effect=retry_handler) + respx_mock.post("/v1/sessions/c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123/act").mock(side_effect=retry_handler) - response = await client.sessions.with_raw_response.start(extra_headers={"x-stainless-retry-count": Omit()}) + response = await client.sessions.with_raw_response.act( + id="c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123", + input="Click the login button", + extra_headers={"x-stainless-retry-count": Omit()}, + ) assert len(response.http_request.headers.get_list("x-stainless-retry-count")) == 0 @@ -1929,9 +1957,13 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: return httpx.Response(500) return httpx.Response(200) - respx_mock.post("/sessions/start").mock(side_effect=retry_handler) + respx_mock.post("/v1/sessions/c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123/act").mock(side_effect=retry_handler) - response = await client.sessions.with_raw_response.start(extra_headers={"x-stainless-retry-count": "42"}) + response = await client.sessions.with_raw_response.act( + id="c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123", + input="Click the login button", + extra_headers={"x-stainless-retry-count": "42"}, + ) assert response.http_request.headers.get("x-stainless-retry-count") == "42" From e5163f96488ebdd3583d5fb03cffcd9f15ad44e2 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 17 Dec 2025 18:22:28 +0000 Subject: [PATCH 33/88] feat(api): manual updates --- .stats.yml | 6 ++-- README.md | 33 +++++++++----------- tests/test_client.py | 72 +++++++++++++++----------------------------- 3 files changed, 41 insertions(+), 70 deletions(-) diff --git a/.stats.yml b/.stats.yml index 87bd24b5..a5338ddc 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 7 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fstagehand-349e1b0f6291eedd731c1660155a50adcb3424fb8cd9e17bbdc0939ff3bbffcd.yml -openapi_spec_hash: 456b593ea71d72bc31a6338a25363e9f -config_hash: 5f6b5ec6e84fb01932ba87c6a9623d9b +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fstagehand-e6a9dca1a93568e403ac72128d86f30c8c3f1336d4b67017d7e61b1836f10f47.yml +openapi_spec_hash: ef01e0649bb0e283df0aa81c369649df +config_hash: abc9d1eb9779bb5629eaed7074c42809 diff --git a/README.md b/README.md index 84c60a60..af522616 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ client = Stagehand( response = client.sessions.act( id="c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123", - input="Click the login button", + input="click the first link on the page", ) print(response.data) ``` @@ -76,7 +76,7 @@ client = AsyncStagehand( async def main() -> None: response = await client.sessions.act( id="c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123", - input="Click the login button", + input="click the first link on the page", ) print(response.data) @@ -119,7 +119,7 @@ async def main() -> None: ) as client: response = await client.sessions.act( id="c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123", - input="Click the login button", + input="click the first link on the page", ) print(response.data) @@ -169,9 +169,8 @@ from stagehand import Stagehand client = Stagehand() try: - client.sessions.act( - id="c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123", - input="Click the login button", + client.sessions.start( + model_name="gpt-4o", ) except stagehand.APIConnectionError as e: print("The server could not be reached") @@ -215,9 +214,8 @@ client = Stagehand( ) # Or, configure per-request: -client.with_options(max_retries=5).sessions.act( - id="c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123", - input="Click the login button", +client.with_options(max_retries=5).sessions.start( + model_name="gpt-4o", ) ``` @@ -241,9 +239,8 @@ client = Stagehand( ) # Override per-request: -client.with_options(timeout=5.0).sessions.act( - id="c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123", - input="Click the login button", +client.with_options(timeout=5.0).sessions.start( + model_name="gpt-4o", ) ``` @@ -285,13 +282,12 @@ The "raw" Response object can be accessed by prefixing `.with_raw_response.` to from stagehand import Stagehand client = Stagehand() -response = client.sessions.with_raw_response.act( - id="c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123", - input="Click the login button", +response = client.sessions.with_raw_response.start( + model_name="gpt-4o", ) print(response.headers.get('X-My-Header')) -session = response.parse() # get the object that `sessions.act()` would have returned +session = response.parse() # get the object that `sessions.start()` would have returned print(session.data) ``` @@ -306,9 +302,8 @@ The above interface eagerly reads the full response body when you make the reque To stream the response body, use `.with_streaming_response` instead, which requires a context manager and only reads the response body once you call `.read()`, `.text()`, `.json()`, `.iter_bytes()`, `.iter_text()`, `.iter_lines()` or `.parse()`. In the async client, these are async methods. ```python -with client.sessions.with_streaming_response.act( - id="c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123", - input="Click the login button", +with client.sessions.with_streaming_response.start( + model_name="gpt-4o", ) as response: print(response.headers.get("X-My-Header")) diff --git a/tests/test_client.py b/tests/test_client.py index df47cdaf..8918f89f 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -876,26 +876,20 @@ def test_parse_retry_after_header( @mock.patch("stagehand._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) @pytest.mark.respx(base_url=base_url) def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter, client: Stagehand) -> None: - respx_mock.post("/v1/sessions/c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123/act").mock( - side_effect=httpx.TimeoutException("Test timeout error") - ) + respx_mock.post("/v1/sessions/start").mock(side_effect=httpx.TimeoutException("Test timeout error")) with pytest.raises(APITimeoutError): - client.sessions.with_streaming_response.act( - id="c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123", input="Click the login button" - ).__enter__() + client.sessions.with_streaming_response.start(model_name="gpt-4o").__enter__() assert _get_open_connections(client) == 0 @mock.patch("stagehand._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) @pytest.mark.respx(base_url=base_url) def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter, client: Stagehand) -> None: - respx_mock.post("/v1/sessions/c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123/act").mock(return_value=httpx.Response(500)) + respx_mock.post("/v1/sessions/start").mock(return_value=httpx.Response(500)) with pytest.raises(APIStatusError): - client.sessions.with_streaming_response.act( - id="c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123", input="Click the login button" - ).__enter__() + client.sessions.with_streaming_response.start(model_name="gpt-4o").__enter__() assert _get_open_connections(client) == 0 @pytest.mark.parametrize("failures_before_success", [0, 2, 4]) @@ -922,11 +916,9 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: return httpx.Response(500) return httpx.Response(200) - respx_mock.post("/v1/sessions/c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123/act").mock(side_effect=retry_handler) + respx_mock.post("/v1/sessions/start").mock(side_effect=retry_handler) - response = client.sessions.with_raw_response.act( - id="c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123", input="Click the login button" - ) + response = client.sessions.with_raw_response.start(model_name="gpt-4o") assert response.retries_taken == failures_before_success assert int(response.http_request.headers.get("x-stainless-retry-count")) == failures_before_success @@ -948,12 +940,10 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: return httpx.Response(500) return httpx.Response(200) - respx_mock.post("/v1/sessions/c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123/act").mock(side_effect=retry_handler) + respx_mock.post("/v1/sessions/start").mock(side_effect=retry_handler) - response = client.sessions.with_raw_response.act( - id="c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123", - input="Click the login button", - extra_headers={"x-stainless-retry-count": Omit()}, + response = client.sessions.with_raw_response.start( + model_name="gpt-4o", extra_headers={"x-stainless-retry-count": Omit()} ) assert len(response.http_request.headers.get_list("x-stainless-retry-count")) == 0 @@ -975,12 +965,10 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: return httpx.Response(500) return httpx.Response(200) - respx_mock.post("/v1/sessions/c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123/act").mock(side_effect=retry_handler) + respx_mock.post("/v1/sessions/start").mock(side_effect=retry_handler) - response = client.sessions.with_raw_response.act( - id="c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123", - input="Click the login button", - extra_headers={"x-stainless-retry-count": "42"}, + response = client.sessions.with_raw_response.start( + model_name="gpt-4o", extra_headers={"x-stainless-retry-count": "42"} ) assert response.http_request.headers.get("x-stainless-retry-count") == "42" @@ -1856,14 +1844,10 @@ async def test_parse_retry_after_header( async def test_retrying_timeout_errors_doesnt_leak( self, respx_mock: MockRouter, async_client: AsyncStagehand ) -> None: - respx_mock.post("/v1/sessions/c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123/act").mock( - side_effect=httpx.TimeoutException("Test timeout error") - ) + respx_mock.post("/v1/sessions/start").mock(side_effect=httpx.TimeoutException("Test timeout error")) with pytest.raises(APITimeoutError): - await async_client.sessions.with_streaming_response.act( - id="c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123", input="Click the login button" - ).__aenter__() + await async_client.sessions.with_streaming_response.start(model_name="gpt-4o").__aenter__() assert _get_open_connections(async_client) == 0 @@ -1872,12 +1856,10 @@ async def test_retrying_timeout_errors_doesnt_leak( async def test_retrying_status_errors_doesnt_leak( self, respx_mock: MockRouter, async_client: AsyncStagehand ) -> None: - respx_mock.post("/v1/sessions/c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123/act").mock(return_value=httpx.Response(500)) + respx_mock.post("/v1/sessions/start").mock(return_value=httpx.Response(500)) with pytest.raises(APIStatusError): - await async_client.sessions.with_streaming_response.act( - id="c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123", input="Click the login button" - ).__aenter__() + await async_client.sessions.with_streaming_response.start(model_name="gpt-4o").__aenter__() assert _get_open_connections(async_client) == 0 @pytest.mark.parametrize("failures_before_success", [0, 2, 4]) @@ -1904,11 +1886,9 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: return httpx.Response(500) return httpx.Response(200) - respx_mock.post("/v1/sessions/c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123/act").mock(side_effect=retry_handler) + respx_mock.post("/v1/sessions/start").mock(side_effect=retry_handler) - response = await client.sessions.with_raw_response.act( - id="c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123", input="Click the login button" - ) + response = await client.sessions.with_raw_response.start(model_name="gpt-4o") assert response.retries_taken == failures_before_success assert int(response.http_request.headers.get("x-stainless-retry-count")) == failures_before_success @@ -1930,12 +1910,10 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: return httpx.Response(500) return httpx.Response(200) - respx_mock.post("/v1/sessions/c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123/act").mock(side_effect=retry_handler) + respx_mock.post("/v1/sessions/start").mock(side_effect=retry_handler) - response = await client.sessions.with_raw_response.act( - id="c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123", - input="Click the login button", - extra_headers={"x-stainless-retry-count": Omit()}, + response = await client.sessions.with_raw_response.start( + model_name="gpt-4o", extra_headers={"x-stainless-retry-count": Omit()} ) assert len(response.http_request.headers.get_list("x-stainless-retry-count")) == 0 @@ -1957,12 +1935,10 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: return httpx.Response(500) return httpx.Response(200) - respx_mock.post("/v1/sessions/c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123/act").mock(side_effect=retry_handler) + respx_mock.post("/v1/sessions/start").mock(side_effect=retry_handler) - response = await client.sessions.with_raw_response.act( - id="c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123", - input="Click the login button", - extra_headers={"x-stainless-retry-count": "42"}, + response = await client.sessions.with_raw_response.start( + model_name="gpt-4o", extra_headers={"x-stainless-retry-count": "42"} ) assert response.http_request.headers.get("x-stainless-retry-count") == "42" From fbbdbcbd262dfcbaeb1c2f7a9d54d8fd0ead1e95 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 17 Dec 2025 18:24:31 +0000 Subject: [PATCH 34/88] feat(api): manual updates --- .stats.yml | 2 +- README.md | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.stats.yml b/.stats.yml index a5338ddc..4bcf91f7 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 7 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fstagehand-e6a9dca1a93568e403ac72128d86f30c8c3f1336d4b67017d7e61b1836f10f47.yml openapi_spec_hash: ef01e0649bb0e283df0aa81c369649df -config_hash: abc9d1eb9779bb5629eaed7074c42809 +config_hash: 0f2a6f228fb92683466c107fce3bcd21 diff --git a/README.md b/README.md index af522616..093c7753 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,7 @@ client = Stagehand( ) response = client.sessions.act( - id="c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123", + id="00000000-your-session-id-000000000000", input="click the first link on the page", ) print(response.data) @@ -75,7 +75,7 @@ client = AsyncStagehand( async def main() -> None: response = await client.sessions.act( - id="c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123", + id="00000000-your-session-id-000000000000", input="click the first link on the page", ) print(response.data) @@ -118,7 +118,7 @@ async def main() -> None: http_client=DefaultAioHttpClient(), ) as client: response = await client.sessions.act( - id="c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123", + id="00000000-your-session-id-000000000000", input="click the first link on the page", ) print(response.data) From 612e7d897c82ade39ccef4150911806d28886d66 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 17 Dec 2025 18:26:50 +0000 Subject: [PATCH 35/88] feat(api): manual updates --- .stats.yml | 2 +- README.md | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.stats.yml b/.stats.yml index 4bcf91f7..27906053 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 7 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fstagehand-e6a9dca1a93568e403ac72128d86f30c8c3f1336d4b67017d7e61b1836f10f47.yml openapi_spec_hash: ef01e0649bb0e283df0aa81c369649df -config_hash: 0f2a6f228fb92683466c107fce3bcd21 +config_hash: 88e87ba7021be93d267ecfc8f5e6b891 diff --git a/README.md b/README.md index 093c7753..683f515e 100644 --- a/README.md +++ b/README.md @@ -170,7 +170,7 @@ client = Stagehand() try: client.sessions.start( - model_name="gpt-4o", + model_name="openai/gpt-5-nano", ) except stagehand.APIConnectionError as e: print("The server could not be reached") @@ -215,7 +215,7 @@ client = Stagehand( # Or, configure per-request: client.with_options(max_retries=5).sessions.start( - model_name="gpt-4o", + model_name="openai/gpt-5-nano", ) ``` @@ -240,7 +240,7 @@ client = Stagehand( # Override per-request: client.with_options(timeout=5.0).sessions.start( - model_name="gpt-4o", + model_name="openai/gpt-5-nano", ) ``` @@ -283,7 +283,7 @@ from stagehand import Stagehand client = Stagehand() response = client.sessions.with_raw_response.start( - model_name="gpt-4o", + model_name="openai/gpt-5-nano", ) print(response.headers.get('X-My-Header')) @@ -303,7 +303,7 @@ To stream the response body, use `.with_streaming_response` instead, which requi ```python with client.sessions.with_streaming_response.start( - model_name="gpt-4o", + model_name="openai/gpt-5-nano", ) as response: print(response.headers.get("X-My-Header")) From fba15525497280ef8a29c454e52f1b74b690bc6c Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 17 Dec 2025 18:46:29 +0000 Subject: [PATCH 36/88] feat(api): manual updates --- .stats.yml | 4 ++-- src/stagehand/resources/sessions.py | 4 ++-- src/stagehand/types/session_act_response.py | 4 ++-- src/stagehand/types/session_execute_response.py | 4 ++-- src/stagehand/types/session_extract_response.py | 4 ++-- src/stagehand/types/session_navigate_response.py | 4 ++-- src/stagehand/types/session_observe_response.py | 4 ++-- src/stagehand/types/session_start_params.py | 2 +- src/stagehand/types/session_start_response.py | 5 ++--- 9 files changed, 17 insertions(+), 18 deletions(-) diff --git a/.stats.yml b/.stats.yml index 27906053..08f0cefe 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 7 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fstagehand-e6a9dca1a93568e403ac72128d86f30c8c3f1336d4b67017d7e61b1836f10f47.yml -openapi_spec_hash: ef01e0649bb0e283df0aa81c369649df +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fstagehand-d571232203ef4e00986a3245224267db6f8aaffdad57780f712e0694dc8d9e37.yml +openapi_spec_hash: d5d635dd7b24a2e1255c6f2a895253ff config_hash: 88e87ba7021be93d267ecfc8f5e6b891 diff --git a/src/stagehand/resources/sessions.py b/src/stagehand/resources/sessions.py index a39547dc..b100a021 100644 --- a/src/stagehand/resources/sessions.py +++ b/src/stagehand/resources/sessions.py @@ -498,7 +498,7 @@ def start( experimental: bool | Omit = omit, self_heal: bool | Omit = omit, system_prompt: str | Omit = omit, - verbose: Literal[0, 1, 2] | Omit = omit, + verbose: int | Omit = omit, wait_for_captcha_solves: bool | Omit = omit, x_language: Literal["typescript", "python", "playground"] | Omit = omit, x_sdk_version: str | Omit = omit, @@ -1044,7 +1044,7 @@ async def start( experimental: bool | Omit = omit, self_heal: bool | Omit = omit, system_prompt: str | Omit = omit, - verbose: Literal[0, 1, 2] | Omit = omit, + verbose: int | Omit = omit, wait_for_captcha_solves: bool | Omit = omit, x_language: Literal["typescript", "python", "playground"] | Omit = omit, x_sdk_version: str | Omit = omit, diff --git a/src/stagehand/types/session_act_response.py b/src/stagehand/types/session_act_response.py index c9ecd059..94649e38 100644 --- a/src/stagehand/types/session_act_response.py +++ b/src/stagehand/types/session_act_response.py @@ -1,7 +1,6 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. from typing import List, Optional -from typing_extensions import Literal from pydantic import Field as FieldInfo @@ -35,4 +34,5 @@ class Data(BaseModel): class SessionActResponse(BaseModel): data: Data - success: Literal[True] + success: bool + """Indicates whether the request was successful""" diff --git a/src/stagehand/types/session_execute_response.py b/src/stagehand/types/session_execute_response.py index dd750071..7dd4a4b0 100644 --- a/src/stagehand/types/session_execute_response.py +++ b/src/stagehand/types/session_execute_response.py @@ -1,7 +1,6 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. from typing import TYPE_CHECKING, Dict, List, Optional -from typing_extensions import Literal from pydantic import Field as FieldInfo @@ -79,4 +78,5 @@ class Data(BaseModel): class SessionExecuteResponse(BaseModel): data: Data - success: Literal[True] + success: bool + """Indicates whether the request was successful""" diff --git a/src/stagehand/types/session_extract_response.py b/src/stagehand/types/session_extract_response.py index 623cd5eb..a30a98f4 100644 --- a/src/stagehand/types/session_extract_response.py +++ b/src/stagehand/types/session_extract_response.py @@ -1,7 +1,6 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. from typing import Optional -from typing_extensions import Literal from pydantic import Field as FieldInfo @@ -21,4 +20,5 @@ class Data(BaseModel): class SessionExtractResponse(BaseModel): data: Data - success: Literal[True] + success: bool + """Indicates whether the request was successful""" diff --git a/src/stagehand/types/session_navigate_response.py b/src/stagehand/types/session_navigate_response.py index f8667686..ed846258 100644 --- a/src/stagehand/types/session_navigate_response.py +++ b/src/stagehand/types/session_navigate_response.py @@ -1,7 +1,6 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. from typing import Optional -from typing_extensions import Literal from pydantic import Field as FieldInfo @@ -21,4 +20,5 @@ class Data(BaseModel): class SessionNavigateResponse(BaseModel): data: Data - success: Literal[True] + success: bool + """Indicates whether the request was successful""" diff --git a/src/stagehand/types/session_observe_response.py b/src/stagehand/types/session_observe_response.py index 32d31c76..6df90a96 100644 --- a/src/stagehand/types/session_observe_response.py +++ b/src/stagehand/types/session_observe_response.py @@ -1,7 +1,6 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. from typing import List, Optional -from typing_extensions import Literal from pydantic import Field as FieldInfo @@ -21,4 +20,5 @@ class Data(BaseModel): class SessionObserveResponse(BaseModel): data: Data - success: Literal[True] + success: bool + """Indicates whether the request was successful""" diff --git a/src/stagehand/types/session_start_params.py b/src/stagehand/types/session_start_params.py index f73ce65a..556fdb37 100644 --- a/src/stagehand/types/session_start_params.py +++ b/src/stagehand/types/session_start_params.py @@ -57,7 +57,7 @@ class SessionStartParams(TypedDict, total=False): system_prompt: Annotated[str, PropertyInfo(alias="systemPrompt")] """Custom system prompt for AI operations""" - verbose: Literal[0, 1, 2] + verbose: int """Logging verbosity level (0=quiet, 1=normal, 2=debug)""" wait_for_captcha_solves: Annotated[bool, PropertyInfo(alias="waitForCaptchaSolves")] diff --git a/src/stagehand/types/session_start_response.py b/src/stagehand/types/session_start_response.py index c6715b1d..c8e74c36 100644 --- a/src/stagehand/types/session_start_response.py +++ b/src/stagehand/types/session_start_response.py @@ -1,7 +1,5 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -from typing_extensions import Literal - from pydantic import Field as FieldInfo from .._models import BaseModel @@ -19,4 +17,5 @@ class Data(BaseModel): class SessionStartResponse(BaseModel): data: Data - success: Literal[True] + success: bool + """Indicates whether the request was successful""" From 11c4c7ce32a62bb4e64ba14cb724cbeb86c89c9d Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 17 Dec 2025 18:56:06 +0000 Subject: [PATCH 37/88] feat(api): manual updates --- .stats.yml | 4 ++-- src/stagehand/resources/sessions.py | 4 ++-- src/stagehand/types/model_config_param.py | 9 ++++++--- src/stagehand/types/session_act_params.py | 4 ++++ src/stagehand/types/session_end_response.py | 5 ++--- src/stagehand/types/session_execute_params.py | 4 ++++ src/stagehand/types/session_extract_params.py | 4 ++++ src/stagehand/types/session_observe_params.py | 4 ++++ src/stagehand/types/session_start_params.py | 2 +- tests/api_resources/test_sessions.py | 16 ++++++++-------- 10 files changed, 37 insertions(+), 19 deletions(-) diff --git a/.stats.yml b/.stats.yml index 08f0cefe..38ef77c8 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 7 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fstagehand-d571232203ef4e00986a3245224267db6f8aaffdad57780f712e0694dc8d9e37.yml -openapi_spec_hash: d5d635dd7b24a2e1255c6f2a895253ff +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fstagehand-817d1d4e845b1946dac8ee10fd34b3f533aa36f74ac598582ffb1b0399a5a932.yml +openapi_spec_hash: 9d856db62b34909fec94743235b3d7be config_hash: 88e87ba7021be93d267ecfc8f5e6b891 diff --git a/src/stagehand/resources/sessions.py b/src/stagehand/resources/sessions.py index b100a021..a39547dc 100644 --- a/src/stagehand/resources/sessions.py +++ b/src/stagehand/resources/sessions.py @@ -498,7 +498,7 @@ def start( experimental: bool | Omit = omit, self_heal: bool | Omit = omit, system_prompt: str | Omit = omit, - verbose: int | Omit = omit, + verbose: Literal[0, 1, 2] | Omit = omit, wait_for_captcha_solves: bool | Omit = omit, x_language: Literal["typescript", "python", "playground"] | Omit = omit, x_sdk_version: str | Omit = omit, @@ -1044,7 +1044,7 @@ async def start( experimental: bool | Omit = omit, self_heal: bool | Omit = omit, system_prompt: str | Omit = omit, - verbose: int | Omit = omit, + verbose: Literal[0, 1, 2] | Omit = omit, wait_for_captcha_solves: bool | Omit = omit, x_language: Literal["typescript", "python", "playground"] | Omit = omit, x_sdk_version: str | Omit = omit, diff --git a/src/stagehand/types/model_config_param.py b/src/stagehand/types/model_config_param.py index 5e897c89..b6026e77 100644 --- a/src/stagehand/types/model_config_param.py +++ b/src/stagehand/types/model_config_param.py @@ -7,15 +7,18 @@ from .._utils import PropertyInfo -__all__ = ["ModelConfigParam", "UnionMember1"] +__all__ = ["ModelConfigParam", "ModelConfigObject"] -class UnionMember1(TypedDict, total=False): +class ModelConfigObject(TypedDict, total=False): model_name: Required[Annotated[str, PropertyInfo(alias="modelName")]] + """Model name string without prefix (e.g., 'gpt-5-nano', 'claude-4.5-opus')""" api_key: Annotated[str, PropertyInfo(alias="apiKey")] + """API key for the model provider""" base_url: Annotated[str, PropertyInfo(alias="baseURL")] + """Base URL for the model provider""" -ModelConfigParam: TypeAlias = Union[str, UnionMember1] +ModelConfigParam: TypeAlias = Union[str, ModelConfigObject] diff --git a/src/stagehand/types/session_act_params.py b/src/stagehand/types/session_act_params.py index 4840ebfd..67c04481 100644 --- a/src/stagehand/types/session_act_params.py +++ b/src/stagehand/types/session_act_params.py @@ -56,6 +56,10 @@ class InputActionInput(TypedDict, total=False): class Options(TypedDict, total=False): model: ModelConfigParam + """ + Model name string with provider prefix (e.g., 'openai/gpt-5-nano', + 'anthropic/claude-4.5-opus') + """ timeout: float """Timeout in ms for the action""" diff --git a/src/stagehand/types/session_end_response.py b/src/stagehand/types/session_end_response.py index 7e538b37..19279b57 100644 --- a/src/stagehand/types/session_end_response.py +++ b/src/stagehand/types/session_end_response.py @@ -1,11 +1,10 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -from typing_extensions import Literal - from .._models import BaseModel __all__ = ["SessionEndResponse"] class SessionEndResponse(BaseModel): - success: Literal[True] + success: bool + """Indicates whether the request was successful""" diff --git a/src/stagehand/types/session_execute_params.py b/src/stagehand/types/session_execute_params.py index 38fc1c3b..458f85bc 100644 --- a/src/stagehand/types/session_execute_params.py +++ b/src/stagehand/types/session_execute_params.py @@ -38,6 +38,10 @@ class AgentConfig(TypedDict, total=False): """Enable Computer Use Agent mode""" model: ModelConfigParam + """ + Model name string with provider prefix (e.g., 'openai/gpt-5-nano', + 'anthropic/claude-4.5-opus') + """ system_prompt: Annotated[str, PropertyInfo(alias="systemPrompt")] """Custom system prompt for the agent""" diff --git a/src/stagehand/types/session_extract_params.py b/src/stagehand/types/session_extract_params.py index 22fb4863..a62e3eb8 100644 --- a/src/stagehand/types/session_extract_params.py +++ b/src/stagehand/types/session_extract_params.py @@ -39,6 +39,10 @@ class SessionExtractParams(TypedDict, total=False): class Options(TypedDict, total=False): model: ModelConfigParam + """ + Model name string with provider prefix (e.g., 'openai/gpt-5-nano', + 'anthropic/claude-4.5-opus') + """ selector: str """CSS selector to scope extraction to a specific element""" diff --git a/src/stagehand/types/session_observe_params.py b/src/stagehand/types/session_observe_params.py index b915cfb8..089db6b1 100644 --- a/src/stagehand/types/session_observe_params.py +++ b/src/stagehand/types/session_observe_params.py @@ -36,6 +36,10 @@ class SessionObserveParams(TypedDict, total=False): class Options(TypedDict, total=False): model: ModelConfigParam + """ + Model name string with provider prefix (e.g., 'openai/gpt-5-nano', + 'anthropic/claude-4.5-opus') + """ selector: str """CSS selector to scope observation to a specific element""" diff --git a/src/stagehand/types/session_start_params.py b/src/stagehand/types/session_start_params.py index 556fdb37..f73ce65a 100644 --- a/src/stagehand/types/session_start_params.py +++ b/src/stagehand/types/session_start_params.py @@ -57,7 +57,7 @@ class SessionStartParams(TypedDict, total=False): system_prompt: Annotated[str, PropertyInfo(alias="systemPrompt")] """Custom system prompt for AI operations""" - verbose: int + verbose: Literal[0, 1, 2] """Logging verbosity level (0=quiet, 1=normal, 2=debug)""" wait_for_captcha_solves: Annotated[bool, PropertyInfo(alias="waitForCaptchaSolves")] diff --git a/tests/api_resources/test_sessions.py b/tests/api_resources/test_sessions.py index 29e86eb2..dcea4de6 100644 --- a/tests/api_resources/test_sessions.py +++ b/tests/api_resources/test_sessions.py @@ -43,7 +43,7 @@ def test_method_act_with_all_params(self, client: Stagehand) -> None: input="Click the login button", frame_id="frameId", options={ - "model": "string", + "model": "openai/gpt-5-nano", "timeout": 30000, "variables": {"username": "john_doe"}, }, @@ -164,7 +164,7 @@ def test_method_execute_with_all_params(self, client: Stagehand) -> None: id="c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123", agent_config={ "cua": True, - "model": "string", + "model": "openai/gpt-5-nano", "system_prompt": "systemPrompt", }, execute_options={ @@ -242,7 +242,7 @@ def test_method_extract_with_all_params(self, client: Stagehand) -> None: frame_id="frameId", instruction="Extract all product names and prices from the page", options={ - "model": "string", + "model": "openai/gpt-5-nano", "selector": "#main-content", "timeout": 30000, }, @@ -369,7 +369,7 @@ def test_method_observe_with_all_params(self, client: Stagehand) -> None: frame_id="frameId", instruction="Find all clickable navigation links", options={ - "model": "string", + "model": "openai/gpt-5-nano", "selector": "nav", "timeout": 30000, }, @@ -562,7 +562,7 @@ async def test_method_act_with_all_params(self, async_client: AsyncStagehand) -> input="Click the login button", frame_id="frameId", options={ - "model": "string", + "model": "openai/gpt-5-nano", "timeout": 30000, "variables": {"username": "john_doe"}, }, @@ -683,7 +683,7 @@ async def test_method_execute_with_all_params(self, async_client: AsyncStagehand id="c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123", agent_config={ "cua": True, - "model": "string", + "model": "openai/gpt-5-nano", "system_prompt": "systemPrompt", }, execute_options={ @@ -761,7 +761,7 @@ async def test_method_extract_with_all_params(self, async_client: AsyncStagehand frame_id="frameId", instruction="Extract all product names and prices from the page", options={ - "model": "string", + "model": "openai/gpt-5-nano", "selector": "#main-content", "timeout": 30000, }, @@ -888,7 +888,7 @@ async def test_method_observe_with_all_params(self, async_client: AsyncStagehand frame_id="frameId", instruction="Find all clickable navigation links", options={ - "model": "string", + "model": "openai/gpt-5-nano", "selector": "nav", "timeout": 30000, }, From a98fd80ffbf5c56e66d968500429e31554e5cb22 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 17 Dec 2025 19:06:26 +0000 Subject: [PATCH 38/88] codegen metadata --- .stats.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index 38ef77c8..65bd7349 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 7 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fstagehand-817d1d4e845b1946dac8ee10fd34b3f533aa36f74ac598582ffb1b0399a5a932.yml -openapi_spec_hash: 9d856db62b34909fec94743235b3d7be +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fstagehand-e09ad013e10b6a6bb76dd9b2067696ba92bd7acb862b39bbfe2f296ebdb6eddf.yml +openapi_spec_hash: 2f633591561e4737534842273441a818 config_hash: 88e87ba7021be93d267ecfc8f5e6b891 From 348ea7da52ee6f2cb545cc3b81d70bbf95a616fb Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 17 Dec 2025 20:09:09 +0000 Subject: [PATCH 39/88] feat(api): manual updates --- .stats.yml | 6 ++--- src/stagehand/resources/sessions.py | 4 +-- src/stagehand/types/session_start_params.py | 26 +++++++++---------- tests/api_resources/test_sessions.py | 28 ++++++++++----------- 4 files changed, 32 insertions(+), 32 deletions(-) diff --git a/.stats.yml b/.stats.yml index 65bd7349..604325f2 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 7 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fstagehand-e09ad013e10b6a6bb76dd9b2067696ba92bd7acb862b39bbfe2f296ebdb6eddf.yml -openapi_spec_hash: 2f633591561e4737534842273441a818 -config_hash: 88e87ba7021be93d267ecfc8f5e6b891 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fstagehand-e96507dd78e76fccc77ba7fb09704da127ead6f4d73ea854e9b2150e90787ff4.yml +openapi_spec_hash: 0c2548b8fdd6de6789b19123e69609c1 +config_hash: c3abb41dbe698d59b3bf12f393013d54 diff --git a/src/stagehand/resources/sessions.py b/src/stagehand/resources/sessions.py index a39547dc..b100a021 100644 --- a/src/stagehand/resources/sessions.py +++ b/src/stagehand/resources/sessions.py @@ -498,7 +498,7 @@ def start( experimental: bool | Omit = omit, self_heal: bool | Omit = omit, system_prompt: str | Omit = omit, - verbose: Literal[0, 1, 2] | Omit = omit, + verbose: int | Omit = omit, wait_for_captcha_solves: bool | Omit = omit, x_language: Literal["typescript", "python", "playground"] | Omit = omit, x_sdk_version: str | Omit = omit, @@ -1044,7 +1044,7 @@ async def start( experimental: bool | Omit = omit, self_heal: bool | Omit = omit, system_prompt: str | Omit = omit, - verbose: Literal[0, 1, 2] | Omit = omit, + verbose: int | Omit = omit, wait_for_captcha_solves: bool | Omit = omit, x_language: Literal["typescript", "python", "playground"] | Omit = omit, x_sdk_version: str | Omit = omit, diff --git a/src/stagehand/types/session_start_params.py b/src/stagehand/types/session_start_params.py index f73ce65a..50f0538e 100644 --- a/src/stagehand/types/session_start_params.py +++ b/src/stagehand/types/session_start_params.py @@ -21,10 +21,10 @@ "BrowserbaseSessionCreateParamsBrowserSettingsFingerprint", "BrowserbaseSessionCreateParamsBrowserSettingsFingerprintScreen", "BrowserbaseSessionCreateParamsBrowserSettingsViewport", - "BrowserbaseSessionCreateParamsProxiesUnionMember1", - "BrowserbaseSessionCreateParamsProxiesUnionMember1BrowserbaseProxyConfig", - "BrowserbaseSessionCreateParamsProxiesUnionMember1BrowserbaseProxyConfigGeolocation", - "BrowserbaseSessionCreateParamsProxiesUnionMember1ExternalProxyConfig", + "BrowserbaseSessionCreateParamsProxiesProxyConfigList", + "BrowserbaseSessionCreateParamsProxiesProxyConfigListBrowserbaseProxyConfig", + "BrowserbaseSessionCreateParamsProxiesProxyConfigListBrowserbaseProxyConfigGeolocation", + "BrowserbaseSessionCreateParamsProxiesProxyConfigListExternalProxyConfig", ] @@ -57,7 +57,7 @@ class SessionStartParams(TypedDict, total=False): system_prompt: Annotated[str, PropertyInfo(alias="systemPrompt")] """Custom system prompt for AI operations""" - verbose: Literal[0, 1, 2] + verbose: int """Logging verbosity level (0=quiet, 1=normal, 2=debug)""" wait_for_captcha_solves: Annotated[bool, PropertyInfo(alias="waitForCaptchaSolves")] @@ -197,7 +197,7 @@ class BrowserbaseSessionCreateParamsBrowserSettings(TypedDict, total=False): viewport: BrowserbaseSessionCreateParamsBrowserSettingsViewport -class BrowserbaseSessionCreateParamsProxiesUnionMember1BrowserbaseProxyConfigGeolocation(TypedDict, total=False): +class BrowserbaseSessionCreateParamsProxiesProxyConfigListBrowserbaseProxyConfigGeolocation(TypedDict, total=False): country: Required[str] city: str @@ -205,15 +205,15 @@ class BrowserbaseSessionCreateParamsProxiesUnionMember1BrowserbaseProxyConfigGeo state: str -class BrowserbaseSessionCreateParamsProxiesUnionMember1BrowserbaseProxyConfig(TypedDict, total=False): +class BrowserbaseSessionCreateParamsProxiesProxyConfigListBrowserbaseProxyConfig(TypedDict, total=False): type: Required[Literal["browserbase"]] domain_pattern: Annotated[str, PropertyInfo(alias="domainPattern")] - geolocation: BrowserbaseSessionCreateParamsProxiesUnionMember1BrowserbaseProxyConfigGeolocation + geolocation: BrowserbaseSessionCreateParamsProxiesProxyConfigListBrowserbaseProxyConfigGeolocation -class BrowserbaseSessionCreateParamsProxiesUnionMember1ExternalProxyConfig(TypedDict, total=False): +class BrowserbaseSessionCreateParamsProxiesProxyConfigListExternalProxyConfig(TypedDict, total=False): server: Required[str] type: Required[Literal["external"]] @@ -225,9 +225,9 @@ class BrowserbaseSessionCreateParamsProxiesUnionMember1ExternalProxyConfig(Typed username: str -BrowserbaseSessionCreateParamsProxiesUnionMember1: TypeAlias = Union[ - BrowserbaseSessionCreateParamsProxiesUnionMember1BrowserbaseProxyConfig, - BrowserbaseSessionCreateParamsProxiesUnionMember1ExternalProxyConfig, +BrowserbaseSessionCreateParamsProxiesProxyConfigList: TypeAlias = Union[ + BrowserbaseSessionCreateParamsProxiesProxyConfigListBrowserbaseProxyConfig, + BrowserbaseSessionCreateParamsProxiesProxyConfigListExternalProxyConfig, ] @@ -240,7 +240,7 @@ class BrowserbaseSessionCreateParams(TypedDict, total=False): project_id: Annotated[str, PropertyInfo(alias="projectId")] - proxies: Union[bool, Iterable[BrowserbaseSessionCreateParamsProxiesUnionMember1]] + proxies: Union[bool, Iterable[BrowserbaseSessionCreateParamsProxiesProxyConfigList]] region: Literal["us-west-2", "us-east-1", "eu-central-1", "ap-southeast-1"] diff --git a/tests/api_resources/test_sessions.py b/tests/api_resources/test_sessions.py index dcea4de6..1874ca18 100644 --- a/tests/api_resources/test_sessions.py +++ b/tests/api_resources/test_sessions.py @@ -49,7 +49,7 @@ def test_method_act_with_all_params(self, client: Stagehand) -> None: }, x_language="typescript", x_sdk_version="3.0.6", - x_sent_at=parse_datetime("2025-01-15T10:30:00.000Z"), + x_sent_at=parse_datetime("2025-01-15T10:30:00Z"), x_stream_response="true", ) assert_matches_type(SessionActResponse, session, path=["response"]) @@ -106,7 +106,7 @@ def test_method_end_with_all_params(self, client: Stagehand) -> None: id="c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123", x_language="typescript", x_sdk_version="3.0.6", - x_sent_at=parse_datetime("2025-01-15T10:30:00.000Z"), + x_sent_at=parse_datetime("2025-01-15T10:30:00Z"), x_stream_response="true", ) assert_matches_type(SessionEndResponse, session, path=["response"]) @@ -175,7 +175,7 @@ def test_method_execute_with_all_params(self, client: Stagehand) -> None: frame_id="frameId", x_language="typescript", x_sdk_version="3.0.6", - x_sent_at=parse_datetime("2025-01-15T10:30:00.000Z"), + x_sent_at=parse_datetime("2025-01-15T10:30:00Z"), x_stream_response="true", ) assert_matches_type(SessionExecuteResponse, session, path=["response"]) @@ -249,7 +249,7 @@ def test_method_extract_with_all_params(self, client: Stagehand) -> None: schema={"foo": "bar"}, x_language="typescript", x_sdk_version="3.0.6", - x_sent_at=parse_datetime("2025-01-15T10:30:00.000Z"), + x_sent_at=parse_datetime("2025-01-15T10:30:00Z"), x_stream_response="true", ) assert_matches_type(SessionExtractResponse, session, path=["response"]) @@ -311,7 +311,7 @@ def test_method_navigate_with_all_params(self, client: Stagehand) -> None: }, x_language="typescript", x_sdk_version="3.0.6", - x_sent_at=parse_datetime("2025-01-15T10:30:00.000Z"), + x_sent_at=parse_datetime("2025-01-15T10:30:00Z"), x_stream_response="true", ) assert_matches_type(SessionNavigateResponse, session, path=["response"]) @@ -375,7 +375,7 @@ def test_method_observe_with_all_params(self, client: Stagehand) -> None: }, x_language="typescript", x_sdk_version="3.0.6", - x_sent_at=parse_datetime("2025-01-15T10:30:00.000Z"), + x_sent_at=parse_datetime("2025-01-15T10:30:00Z"), x_stream_response="true", ) assert_matches_type(SessionObserveResponse, session, path=["response"]) @@ -508,7 +508,7 @@ def test_method_start_with_all_params(self, client: Stagehand) -> None: wait_for_captcha_solves=True, x_language="typescript", x_sdk_version="3.0.6", - x_sent_at=parse_datetime("2025-01-15T10:30:00.000Z"), + x_sent_at=parse_datetime("2025-01-15T10:30:00Z"), x_stream_response="true", ) assert_matches_type(SessionStartResponse, session, path=["response"]) @@ -568,7 +568,7 @@ async def test_method_act_with_all_params(self, async_client: AsyncStagehand) -> }, x_language="typescript", x_sdk_version="3.0.6", - x_sent_at=parse_datetime("2025-01-15T10:30:00.000Z"), + x_sent_at=parse_datetime("2025-01-15T10:30:00Z"), x_stream_response="true", ) assert_matches_type(SessionActResponse, session, path=["response"]) @@ -625,7 +625,7 @@ async def test_method_end_with_all_params(self, async_client: AsyncStagehand) -> id="c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123", x_language="typescript", x_sdk_version="3.0.6", - x_sent_at=parse_datetime("2025-01-15T10:30:00.000Z"), + x_sent_at=parse_datetime("2025-01-15T10:30:00Z"), x_stream_response="true", ) assert_matches_type(SessionEndResponse, session, path=["response"]) @@ -694,7 +694,7 @@ async def test_method_execute_with_all_params(self, async_client: AsyncStagehand frame_id="frameId", x_language="typescript", x_sdk_version="3.0.6", - x_sent_at=parse_datetime("2025-01-15T10:30:00.000Z"), + x_sent_at=parse_datetime("2025-01-15T10:30:00Z"), x_stream_response="true", ) assert_matches_type(SessionExecuteResponse, session, path=["response"]) @@ -768,7 +768,7 @@ async def test_method_extract_with_all_params(self, async_client: AsyncStagehand schema={"foo": "bar"}, x_language="typescript", x_sdk_version="3.0.6", - x_sent_at=parse_datetime("2025-01-15T10:30:00.000Z"), + x_sent_at=parse_datetime("2025-01-15T10:30:00Z"), x_stream_response="true", ) assert_matches_type(SessionExtractResponse, session, path=["response"]) @@ -830,7 +830,7 @@ async def test_method_navigate_with_all_params(self, async_client: AsyncStagehan }, x_language="typescript", x_sdk_version="3.0.6", - x_sent_at=parse_datetime("2025-01-15T10:30:00.000Z"), + x_sent_at=parse_datetime("2025-01-15T10:30:00Z"), x_stream_response="true", ) assert_matches_type(SessionNavigateResponse, session, path=["response"]) @@ -894,7 +894,7 @@ async def test_method_observe_with_all_params(self, async_client: AsyncStagehand }, x_language="typescript", x_sdk_version="3.0.6", - x_sent_at=parse_datetime("2025-01-15T10:30:00.000Z"), + x_sent_at=parse_datetime("2025-01-15T10:30:00Z"), x_stream_response="true", ) assert_matches_type(SessionObserveResponse, session, path=["response"]) @@ -1027,7 +1027,7 @@ async def test_method_start_with_all_params(self, async_client: AsyncStagehand) wait_for_captcha_solves=True, x_language="typescript", x_sdk_version="3.0.6", - x_sent_at=parse_datetime("2025-01-15T10:30:00.000Z"), + x_sent_at=parse_datetime("2025-01-15T10:30:00Z"), x_stream_response="true", ) assert_matches_type(SessionStartResponse, session, path=["response"]) From d5a94a8a5e99973f656d41801d4ec0966ea3c4c9 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 18 Dec 2025 01:39:00 +0000 Subject: [PATCH 40/88] feat(api): manual updates --- .stats.yml | 6 +- api.md | 1 + src/stagehand/_client.py | 4 + src/stagehand/_streaming.py | 42 +- src/stagehand/resources/sessions.py | 1546 ++++++++++++++--- src/stagehand/types/__init__.py | 2 + src/stagehand/types/action_param.py | 25 + src/stagehand/types/session_act_params.py | 37 +- src/stagehand/types/session_execute_params.py | 23 +- src/stagehand/types/session_extract_params.py | 19 +- .../types/session_navigate_params.py | 3 + src/stagehand/types/session_observe_params.py | 19 +- src/stagehand/types/session_start_response.py | 5 +- src/stagehand/types/stream_event.py | 44 + tests/api_resources/test_sessions.py | 660 ++++++- tests/test_client.py | 23 + 16 files changed, 2146 insertions(+), 313 deletions(-) create mode 100644 src/stagehand/types/action_param.py create mode 100644 src/stagehand/types/stream_event.py diff --git a/.stats.yml b/.stats.yml index 604325f2..5dba58c6 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 7 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fstagehand-e96507dd78e76fccc77ba7fb09704da127ead6f4d73ea854e9b2150e90787ff4.yml -openapi_spec_hash: 0c2548b8fdd6de6789b19123e69609c1 -config_hash: c3abb41dbe698d59b3bf12f393013d54 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fstagehand-f7d6b6489159f611a2bfdc267ce0a6fc0455bed1ffa0c310044baaa5d8381b9b.yml +openapi_spec_hash: cd88d8068abfde8382da0bed674e440c +config_hash: 5c69fb596588b8ace08203858518c149 diff --git a/api.md b/api.md index cc172ac8..d5ec2705 100644 --- a/api.md +++ b/api.md @@ -6,6 +6,7 @@ Types: from stagehand.types import ( Action, ModelConfig, + StreamEvent, SessionActResponse, SessionEndResponse, SessionExecuteResponse, diff --git a/src/stagehand/_client.py b/src/stagehand/_client.py index b0f365e6..9f93e242 100644 --- a/src/stagehand/_client.py +++ b/src/stagehand/_client.py @@ -124,6 +124,8 @@ def __init__( _strict_response_validation=_strict_response_validation, ) + self._default_stream_cls = Stream + @cached_property def sessions(self) -> SessionsResource: from .resources.sessions import SessionsResource @@ -339,6 +341,8 @@ def __init__( _strict_response_validation=_strict_response_validation, ) + self._default_stream_cls = AsyncStream + @cached_property def sessions(self) -> AsyncSessionsResource: from .resources.sessions import AsyncSessionsResource diff --git a/src/stagehand/_streaming.py b/src/stagehand/_streaming.py index 69a9442b..7790c9c0 100644 --- a/src/stagehand/_streaming.py +++ b/src/stagehand/_streaming.py @@ -56,7 +56,26 @@ def __stream__(self) -> Iterator[_T]: try: for sse in iterator: - yield process_data(data=sse.json(), cast_to=cast_to, response=response) + if sse.data.startswith("finished"): + break + + if sse.data.startswith("error"): + body = sse.data + + try: + body = sse.json() + err_msg = f"{body}" + except Exception: + err_msg = sse.data or f"Error code: {response.status_code}" + + raise self._client._make_status_error( + err_msg, + body=body, + response=self.response, + ) + + if sse.event is None: + yield process_data(data=sse.json(), cast_to=cast_to, response=response) finally: # Ensure the response is closed even if the consumer doesn't read all data response.close() @@ -120,7 +139,26 @@ async def __stream__(self) -> AsyncIterator[_T]: try: async for sse in iterator: - yield process_data(data=sse.json(), cast_to=cast_to, response=response) + if sse.data.startswith("finished"): + break + + if sse.data.startswith("error"): + body = sse.data + + try: + body = sse.json() + err_msg = f"{body}" + except Exception: + err_msg = sse.data or f"Error code: {response.status_code}" + + raise self._client._make_status_error( + err_msg, + body=body, + response=self.response, + ) + + if sse.event is None: + yield process_data(data=sse.json(), cast_to=cast_to, response=response) finally: # Ensure the response is closed even if the consumer doesn't read all data await response.aclose() diff --git a/src/stagehand/resources/sessions.py b/src/stagehand/resources/sessions.py index b100a021..7b317669 100644 --- a/src/stagehand/resources/sessions.py +++ b/src/stagehand/resources/sessions.py @@ -4,7 +4,7 @@ from typing import Dict, Union from datetime import datetime -from typing_extensions import Literal +from typing_extensions import Literal, overload import httpx @@ -17,7 +17,7 @@ session_navigate_params, ) from .._types import Body, Omit, Query, Headers, NotGiven, omit, not_given -from .._utils import is_given, maybe_transform, strip_not_given, async_maybe_transform +from .._utils import is_given, required_args, maybe_transform, strip_not_given, async_maybe_transform from .._compat import cached_property from .._resource import SyncAPIResource, AsyncAPIResource from .._response import ( @@ -26,7 +26,9 @@ async_to_raw_response_wrapper, async_to_streamed_response_wrapper, ) +from .._streaming import Stream, AsyncStream from .._base_client import make_request_options +from ..types.stream_event import StreamEvent from ..types.session_act_response import SessionActResponse from ..types.session_end_response import SessionEndResponse from ..types.session_start_response import SessionStartResponse @@ -58,6 +60,7 @@ def with_streaming_response(self) -> SessionsResourceWithStreamingResponse: """ return SessionsResourceWithStreamingResponse(self) + @overload def act( self, id: str, @@ -65,6 +68,7 @@ def act( input: session_act_params.Input, frame_id: str | Omit = omit, options: session_act_params.Options | Omit = omit, + stream_response: Literal[False] | Omit = omit, x_language: Literal["typescript", "python", "playground"] | Omit = omit, x_sdk_version: str | Omit = omit, x_sent_at: Union[str, datetime] | Omit = omit, @@ -87,6 +91,110 @@ def act( frame_id: Target frame ID for the action + stream_response: Whether to stream the response via SSE + + x_language: Client SDK language + + x_sdk_version: Version of the Stagehand SDK + + x_sent_at: ISO timestamp when request was sent + + x_stream_response: Whether to stream the response via SSE + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + ... + + @overload + def act( + self, + id: str, + *, + input: session_act_params.Input, + stream_response: Literal[True], + frame_id: str | Omit = omit, + options: session_act_params.Options | Omit = omit, + x_language: Literal["typescript", "python", "playground"] | Omit = omit, + x_sdk_version: str | Omit = omit, + x_sent_at: Union[str, datetime] | Omit = omit, + x_stream_response: Literal["true", "false"] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> Stream[StreamEvent]: + """ + Executes a browser action using natural language instructions or a predefined + Action object. + + Args: + id: Unique session identifier + + input: Natural language instruction or Action object + + stream_response: Whether to stream the response via SSE + + frame_id: Target frame ID for the action + + x_language: Client SDK language + + x_sdk_version: Version of the Stagehand SDK + + x_sent_at: ISO timestamp when request was sent + + x_stream_response: Whether to stream the response via SSE + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + ... + + @overload + def act( + self, + id: str, + *, + input: session_act_params.Input, + stream_response: bool, + frame_id: str | Omit = omit, + options: session_act_params.Options | Omit = omit, + x_language: Literal["typescript", "python", "playground"] | Omit = omit, + x_sdk_version: str | Omit = omit, + x_sent_at: Union[str, datetime] | Omit = omit, + x_stream_response: Literal["true", "false"] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> SessionActResponse | Stream[StreamEvent]: + """ + Executes a browser action using natural language instructions or a predefined + Action object. + + Args: + id: Unique session identifier + + input: Natural language instruction or Action object + + stream_response: Whether to stream the response via SSE + + frame_id: Target frame ID for the action + x_language: Client SDK language x_sdk_version: Version of the Stagehand SDK @@ -103,6 +211,28 @@ def act( timeout: Override the client-level default timeout for this request, in seconds """ + ... + + @required_args(["input"], ["input", "stream_response"]) + def act( + self, + id: str, + *, + input: session_act_params.Input, + frame_id: str | Omit = omit, + options: session_act_params.Options | Omit = omit, + stream_response: Literal[False] | Literal[True] | Omit = omit, + x_language: Literal["typescript", "python", "playground"] | Omit = omit, + x_sdk_version: str | Omit = omit, + x_sent_at: Union[str, datetime] | Omit = omit, + x_stream_response: Literal["true", "false"] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> SessionActResponse | Stream[StreamEvent]: if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") extra_headers = { @@ -123,13 +253,18 @@ def act( "input": input, "frame_id": frame_id, "options": options, + "stream_response": stream_response, }, - session_act_params.SessionActParams, + session_act_params.SessionActParamsStreaming + if stream_response + else session_act_params.SessionActParamsNonStreaming, ), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), cast_to=SessionActResponse, + stream=stream_response or False, + stream_cls=Stream[StreamEvent], ) def end( @@ -190,6 +325,7 @@ def end( cast_to=SessionEndResponse, ) + @overload def execute( self, id: str, @@ -197,6 +333,7 @@ def execute( agent_config: session_execute_params.AgentConfig, execute_options: session_execute_params.ExecuteOptions, frame_id: str | Omit = omit, + stream_response: Literal[False] | Omit = omit, x_language: Literal["typescript", "python", "playground"] | Omit = omit, x_sdk_version: str | Omit = omit, x_sent_at: Union[str, datetime] | Omit = omit, @@ -216,6 +353,8 @@ def execute( frame_id: Target frame ID for the agent + stream_response: Whether to stream the response via SSE + x_language: Client SDK language x_sdk_version: Version of the Stagehand SDK @@ -232,43 +371,17 @@ def execute( timeout: Override the client-level default timeout for this request, in seconds """ - if not id: - raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") - extra_headers = { - **strip_not_given( - { - "x-language": str(x_language) if is_given(x_language) else not_given, - "x-sdk-version": x_sdk_version, - "x-sent-at": x_sent_at.isoformat() if is_given(x_sent_at) else not_given, - "x-stream-response": str(x_stream_response) if is_given(x_stream_response) else not_given, - } - ), - **(extra_headers or {}), - } - return self._post( - f"/v1/sessions/{id}/agentExecute", - body=maybe_transform( - { - "agent_config": agent_config, - "execute_options": execute_options, - "frame_id": frame_id, - }, - session_execute_params.SessionExecuteParams, - ), - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=SessionExecuteResponse, - ) + ... - def extract( + @overload + def execute( self, id: str, *, + agent_config: session_execute_params.AgentConfig, + execute_options: session_execute_params.ExecuteOptions, + stream_response: Literal[True], frame_id: str | Omit = omit, - instruction: str | Omit = omit, - options: session_extract_params.Options | Omit = omit, - schema: Dict[str, object] | Omit = omit, x_language: Literal["typescript", "python", "playground"] | Omit = omit, x_sdk_version: str | Omit = omit, x_sent_at: Union[str, datetime] | Omit = omit, @@ -279,18 +392,16 @@ def extract( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> SessionExtractResponse: + ) -> Stream[StreamEvent]: """ - Extracts structured data from the current page using AI-powered analysis. + Runs an autonomous AI agent that can perform complex multi-step browser tasks. Args: id: Unique session identifier - frame_id: Target frame ID for the extraction - - instruction: Natural language instruction for what to extract + stream_response: Whether to stream the response via SSE - schema: JSON Schema defining the structure of data to extract + frame_id: Target frame ID for the agent x_language: Client SDK language @@ -308,43 +419,17 @@ def extract( timeout: Override the client-level default timeout for this request, in seconds """ - if not id: - raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") - extra_headers = { - **strip_not_given( - { - "x-language": str(x_language) if is_given(x_language) else not_given, - "x-sdk-version": x_sdk_version, - "x-sent-at": x_sent_at.isoformat() if is_given(x_sent_at) else not_given, - "x-stream-response": str(x_stream_response) if is_given(x_stream_response) else not_given, - } - ), - **(extra_headers or {}), - } - return self._post( - f"/v1/sessions/{id}/extract", - body=maybe_transform( - { - "frame_id": frame_id, - "instruction": instruction, - "options": options, - "schema": schema, - }, - session_extract_params.SessionExtractParams, - ), - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=SessionExtractResponse, - ) + ... - def navigate( + @overload + def execute( self, id: str, *, - url: str, + agent_config: session_execute_params.AgentConfig, + execute_options: session_execute_params.ExecuteOptions, + stream_response: bool, frame_id: str | Omit = omit, - options: session_navigate_params.Options | Omit = omit, x_language: Literal["typescript", "python", "playground"] | Omit = omit, x_sdk_version: str | Omit = omit, x_sent_at: Union[str, datetime] | Omit = omit, @@ -355,16 +440,16 @@ def navigate( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> SessionNavigateResponse: + ) -> SessionExecuteResponse | Stream[StreamEvent]: """ - Navigates the browser to the specified URL. + Runs an autonomous AI agent that can perform complex multi-step browser tasks. Args: id: Unique session identifier - url: URL to navigate to + stream_response: Whether to stream the response via SSE - frame_id: Target frame ID for the navigation + frame_id: Target frame ID for the agent x_language: Client SDK language @@ -382,6 +467,28 @@ def navigate( timeout: Override the client-level default timeout for this request, in seconds """ + ... + + @required_args(["agent_config", "execute_options"], ["agent_config", "execute_options", "stream_response"]) + def execute( + self, + id: str, + *, + agent_config: session_execute_params.AgentConfig, + execute_options: session_execute_params.ExecuteOptions, + frame_id: str | Omit = omit, + stream_response: Literal[False] | Literal[True] | Omit = omit, + x_language: Literal["typescript", "python", "playground"] | Omit = omit, + x_sdk_version: str | Omit = omit, + x_sent_at: Union[str, datetime] | Omit = omit, + x_stream_response: Literal["true", "false"] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> SessionExecuteResponse | Stream[StreamEvent]: if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") extra_headers = { @@ -396,28 +503,36 @@ def navigate( **(extra_headers or {}), } return self._post( - f"/v1/sessions/{id}/navigate", + f"/v1/sessions/{id}/agentExecute", body=maybe_transform( { - "url": url, + "agent_config": agent_config, + "execute_options": execute_options, "frame_id": frame_id, - "options": options, + "stream_response": stream_response, }, - session_navigate_params.SessionNavigateParams, + session_execute_params.SessionExecuteParamsStreaming + if stream_response + else session_execute_params.SessionExecuteParamsNonStreaming, ), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=SessionNavigateResponse, + cast_to=SessionExecuteResponse, + stream=stream_response or False, + stream_cls=Stream[StreamEvent], ) - def observe( + @overload + def extract( self, id: str, *, frame_id: str | Omit = omit, instruction: str | Omit = omit, - options: session_observe_params.Options | Omit = omit, + options: session_extract_params.Options | Omit = omit, + schema: Dict[str, object] | Omit = omit, + stream_response: Literal[False] | Omit = omit, x_language: Literal["typescript", "python", "playground"] | Omit = omit, x_sdk_version: str | Omit = omit, x_sent_at: Union[str, datetime] | Omit = omit, @@ -428,17 +543,20 @@ def observe( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> SessionObserveResponse: + ) -> SessionExtractResponse: """ - Identifies and returns available actions on the current page that match the - given instruction. + Extracts structured data from the current page using AI-powered analysis. Args: id: Unique session identifier - frame_id: Target frame ID for the observation + frame_id: Target frame ID for the extraction - instruction: Natural language instruction for what actions to find + instruction: Natural language instruction for what to extract + + schema: JSON Schema defining the structure of data to extract + + stream_response: Whether to stream the response via SSE x_language: Client SDK language @@ -456,50 +574,177 @@ def observe( timeout: Override the client-level default timeout for this request, in seconds """ - if not id: - raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") - extra_headers = { - **strip_not_given( - { - "x-language": str(x_language) if is_given(x_language) else not_given, - "x-sdk-version": x_sdk_version, - "x-sent-at": x_sent_at.isoformat() if is_given(x_sent_at) else not_given, - "x-stream-response": str(x_stream_response) if is_given(x_stream_response) else not_given, - } - ), - **(extra_headers or {}), - } - return self._post( - f"/v1/sessions/{id}/observe", - body=maybe_transform( - { - "frame_id": frame_id, + ... + + @overload + def extract( + self, + id: str, + *, + stream_response: Literal[True], + frame_id: str | Omit = omit, + instruction: str | Omit = omit, + options: session_extract_params.Options | Omit = omit, + schema: Dict[str, object] | Omit = omit, + x_language: Literal["typescript", "python", "playground"] | Omit = omit, + x_sdk_version: str | Omit = omit, + x_sent_at: Union[str, datetime] | Omit = omit, + x_stream_response: Literal["true", "false"] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> Stream[StreamEvent]: + """ + Extracts structured data from the current page using AI-powered analysis. + + Args: + id: Unique session identifier + + stream_response: Whether to stream the response via SSE + + frame_id: Target frame ID for the extraction + + instruction: Natural language instruction for what to extract + + schema: JSON Schema defining the structure of data to extract + + x_language: Client SDK language + + x_sdk_version: Version of the Stagehand SDK + + x_sent_at: ISO timestamp when request was sent + + x_stream_response: Whether to stream the response via SSE + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + ... + + @overload + def extract( + self, + id: str, + *, + stream_response: bool, + frame_id: str | Omit = omit, + instruction: str | Omit = omit, + options: session_extract_params.Options | Omit = omit, + schema: Dict[str, object] | Omit = omit, + x_language: Literal["typescript", "python", "playground"] | Omit = omit, + x_sdk_version: str | Omit = omit, + x_sent_at: Union[str, datetime] | Omit = omit, + x_stream_response: Literal["true", "false"] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> SessionExtractResponse | Stream[StreamEvent]: + """ + Extracts structured data from the current page using AI-powered analysis. + + Args: + id: Unique session identifier + + stream_response: Whether to stream the response via SSE + + frame_id: Target frame ID for the extraction + + instruction: Natural language instruction for what to extract + + schema: JSON Schema defining the structure of data to extract + + x_language: Client SDK language + + x_sdk_version: Version of the Stagehand SDK + + x_sent_at: ISO timestamp when request was sent + + x_stream_response: Whether to stream the response via SSE + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + ... + + def extract( + self, + id: str, + *, + frame_id: str | Omit = omit, + instruction: str | Omit = omit, + options: session_extract_params.Options | Omit = omit, + schema: Dict[str, object] | Omit = omit, + stream_response: Literal[False] | Literal[True] | Omit = omit, + x_language: Literal["typescript", "python", "playground"] | Omit = omit, + x_sdk_version: str | Omit = omit, + x_sent_at: Union[str, datetime] | Omit = omit, + x_stream_response: Literal["true", "false"] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> SessionExtractResponse | Stream[StreamEvent]: + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + extra_headers = { + **strip_not_given( + { + "x-language": str(x_language) if is_given(x_language) else not_given, + "x-sdk-version": x_sdk_version, + "x-sent-at": x_sent_at.isoformat() if is_given(x_sent_at) else not_given, + "x-stream-response": str(x_stream_response) if is_given(x_stream_response) else not_given, + } + ), + **(extra_headers or {}), + } + return self._post( + f"/v1/sessions/{id}/extract", + body=maybe_transform( + { + "frame_id": frame_id, "instruction": instruction, "options": options, + "schema": schema, + "stream_response": stream_response, }, - session_observe_params.SessionObserveParams, + session_extract_params.SessionExtractParamsStreaming + if stream_response + else session_extract_params.SessionExtractParamsNonStreaming, ), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=SessionObserveResponse, + cast_to=SessionExtractResponse, + stream=stream_response or False, + stream_cls=Stream[StreamEvent], ) - def start( + def navigate( self, + id: str, *, - model_name: str, - act_timeout_ms: float | Omit = omit, - browser: session_start_params.Browser | Omit = omit, - browserbase_session_create_params: session_start_params.BrowserbaseSessionCreateParams | Omit = omit, - browserbase_session_id: str | Omit = omit, - debug_dom: bool | Omit = omit, - dom_settle_timeout_ms: float | Omit = omit, - experimental: bool | Omit = omit, - self_heal: bool | Omit = omit, - system_prompt: str | Omit = omit, - verbose: int | Omit = omit, - wait_for_captcha_solves: bool | Omit = omit, + url: str, + frame_id: str | Omit = omit, + options: session_navigate_params.Options | Omit = omit, + stream_response: bool | Omit = omit, x_language: Literal["typescript", "python", "playground"] | Omit = omit, x_sdk_version: str | Omit = omit, x_sent_at: Union[str, datetime] | Omit = omit, @@ -510,26 +755,18 @@ def start( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> SessionStartResponse: - """Creates a new browser session with the specified configuration. - - Returns a - session ID used for all subsequent operations. + ) -> SessionNavigateResponse: + """ + Navigates the browser to the specified URL. Args: - model_name: Model name to use for AI operations - - act_timeout_ms: Timeout in ms for act operations - - browserbase_session_id: Existing Browserbase session ID to resume - - dom_settle_timeout_ms: Timeout in ms to wait for DOM to settle + id: Unique session identifier - self_heal: Enable self-healing for failed actions + url: URL to navigate to - system_prompt: Custom system prompt for AI operations + frame_id: Target frame ID for the navigation - verbose: Logging verbosity level (0=quiet, 1=normal, 2=debug) + stream_response: Whether to stream the response via SSE x_language: Client SDK language @@ -547,6 +784,8 @@ def start( timeout: Override the client-level default timeout for this request, in seconds """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") extra_headers = { **strip_not_given( { @@ -559,58 +798,717 @@ def start( **(extra_headers or {}), } return self._post( - "/v1/sessions/start", + f"/v1/sessions/{id}/navigate", body=maybe_transform( { - "model_name": model_name, - "act_timeout_ms": act_timeout_ms, - "browser": browser, - "browserbase_session_create_params": browserbase_session_create_params, - "browserbase_session_id": browserbase_session_id, - "debug_dom": debug_dom, - "dom_settle_timeout_ms": dom_settle_timeout_ms, - "experimental": experimental, - "self_heal": self_heal, - "system_prompt": system_prompt, - "verbose": verbose, - "wait_for_captcha_solves": wait_for_captcha_solves, + "url": url, + "frame_id": frame_id, + "options": options, + "stream_response": stream_response, }, - session_start_params.SessionStartParams, + session_navigate_params.SessionNavigateParams, ), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=SessionStartResponse, + cast_to=SessionNavigateResponse, ) - -class AsyncSessionsResource(AsyncAPIResource): - @cached_property - def with_raw_response(self) -> AsyncSessionsResourceWithRawResponse: + @overload + def observe( + self, + id: str, + *, + frame_id: str | Omit = omit, + instruction: str | Omit = omit, + options: session_observe_params.Options | Omit = omit, + stream_response: Literal[False] | Omit = omit, + x_language: Literal["typescript", "python", "playground"] | Omit = omit, + x_sdk_version: str | Omit = omit, + x_sent_at: Union[str, datetime] | Omit = omit, + x_stream_response: Literal["true", "false"] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> SessionObserveResponse: """ - This property can be used as a prefix for any HTTP method call to return - the raw response object instead of the parsed content. + Identifies and returns available actions on the current page that match the + given instruction. - For more information, see https://www.github.com/browserbase/stagehand-python#accessing-raw-response-data-eg-headers - """ - return AsyncSessionsResourceWithRawResponse(self) + Args: + id: Unique session identifier - @cached_property - def with_streaming_response(self) -> AsyncSessionsResourceWithStreamingResponse: + frame_id: Target frame ID for the observation + + instruction: Natural language instruction for what actions to find + + stream_response: Whether to stream the response via SSE + + x_language: Client SDK language + + x_sdk_version: Version of the Stagehand SDK + + x_sent_at: ISO timestamp when request was sent + + x_stream_response: Whether to stream the response via SSE + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds """ - An alternative to `.with_raw_response` that doesn't eagerly read the response body. + ... + + @overload + def observe( + self, + id: str, + *, + stream_response: Literal[True], + frame_id: str | Omit = omit, + instruction: str | Omit = omit, + options: session_observe_params.Options | Omit = omit, + x_language: Literal["typescript", "python", "playground"] | Omit = omit, + x_sdk_version: str | Omit = omit, + x_sent_at: Union[str, datetime] | Omit = omit, + x_stream_response: Literal["true", "false"] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> Stream[StreamEvent]: + """ + Identifies and returns available actions on the current page that match the + given instruction. + + Args: + id: Unique session identifier + + stream_response: Whether to stream the response via SSE + + frame_id: Target frame ID for the observation + + instruction: Natural language instruction for what actions to find + + x_language: Client SDK language + + x_sdk_version: Version of the Stagehand SDK + + x_sent_at: ISO timestamp when request was sent + + x_stream_response: Whether to stream the response via SSE + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + ... + + @overload + def observe( + self, + id: str, + *, + stream_response: bool, + frame_id: str | Omit = omit, + instruction: str | Omit = omit, + options: session_observe_params.Options | Omit = omit, + x_language: Literal["typescript", "python", "playground"] | Omit = omit, + x_sdk_version: str | Omit = omit, + x_sent_at: Union[str, datetime] | Omit = omit, + x_stream_response: Literal["true", "false"] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> SessionObserveResponse | Stream[StreamEvent]: + """ + Identifies and returns available actions on the current page that match the + given instruction. + + Args: + id: Unique session identifier + + stream_response: Whether to stream the response via SSE + + frame_id: Target frame ID for the observation + + instruction: Natural language instruction for what actions to find + + x_language: Client SDK language + + x_sdk_version: Version of the Stagehand SDK + + x_sent_at: ISO timestamp when request was sent + + x_stream_response: Whether to stream the response via SSE + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + ... + + def observe( + self, + id: str, + *, + frame_id: str | Omit = omit, + instruction: str | Omit = omit, + options: session_observe_params.Options | Omit = omit, + stream_response: Literal[False] | Literal[True] | Omit = omit, + x_language: Literal["typescript", "python", "playground"] | Omit = omit, + x_sdk_version: str | Omit = omit, + x_sent_at: Union[str, datetime] | Omit = omit, + x_stream_response: Literal["true", "false"] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> SessionObserveResponse | Stream[StreamEvent]: + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + extra_headers = { + **strip_not_given( + { + "x-language": str(x_language) if is_given(x_language) else not_given, + "x-sdk-version": x_sdk_version, + "x-sent-at": x_sent_at.isoformat() if is_given(x_sent_at) else not_given, + "x-stream-response": str(x_stream_response) if is_given(x_stream_response) else not_given, + } + ), + **(extra_headers or {}), + } + return self._post( + f"/v1/sessions/{id}/observe", + body=maybe_transform( + { + "frame_id": frame_id, + "instruction": instruction, + "options": options, + "stream_response": stream_response, + }, + session_observe_params.SessionObserveParamsStreaming + if stream_response + else session_observe_params.SessionObserveParamsNonStreaming, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=SessionObserveResponse, + stream=stream_response or False, + stream_cls=Stream[StreamEvent], + ) + + def start( + self, + *, + model_name: str, + act_timeout_ms: float | Omit = omit, + browser: session_start_params.Browser | Omit = omit, + browserbase_session_create_params: session_start_params.BrowserbaseSessionCreateParams | Omit = omit, + browserbase_session_id: str | Omit = omit, + debug_dom: bool | Omit = omit, + dom_settle_timeout_ms: float | Omit = omit, + experimental: bool | Omit = omit, + self_heal: bool | Omit = omit, + system_prompt: str | Omit = omit, + verbose: int | Omit = omit, + wait_for_captcha_solves: bool | Omit = omit, + x_language: Literal["typescript", "python", "playground"] | Omit = omit, + x_sdk_version: str | Omit = omit, + x_sent_at: Union[str, datetime] | Omit = omit, + x_stream_response: Literal["true", "false"] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> SessionStartResponse: + """Creates a new browser session with the specified configuration. + + Returns a + session ID used for all subsequent operations. + + Args: + model_name: Model name to use for AI operations + + act_timeout_ms: Timeout in ms for act operations + + browserbase_session_id: Existing Browserbase session ID to resume + + dom_settle_timeout_ms: Timeout in ms to wait for DOM to settle + + self_heal: Enable self-healing for failed actions + + system_prompt: Custom system prompt for AI operations + + verbose: Logging verbosity level (0=quiet, 1=normal, 2=debug) + + x_language: Client SDK language + + x_sdk_version: Version of the Stagehand SDK + + x_sent_at: ISO timestamp when request was sent + + x_stream_response: Whether to stream the response via SSE + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + extra_headers = { + **strip_not_given( + { + "x-language": str(x_language) if is_given(x_language) else not_given, + "x-sdk-version": x_sdk_version, + "x-sent-at": x_sent_at.isoformat() if is_given(x_sent_at) else not_given, + "x-stream-response": str(x_stream_response) if is_given(x_stream_response) else not_given, + } + ), + **(extra_headers or {}), + } + return self._post( + "/v1/sessions/start", + body=maybe_transform( + { + "model_name": model_name, + "act_timeout_ms": act_timeout_ms, + "browser": browser, + "browserbase_session_create_params": browserbase_session_create_params, + "browserbase_session_id": browserbase_session_id, + "debug_dom": debug_dom, + "dom_settle_timeout_ms": dom_settle_timeout_ms, + "experimental": experimental, + "self_heal": self_heal, + "system_prompt": system_prompt, + "verbose": verbose, + "wait_for_captcha_solves": wait_for_captcha_solves, + }, + session_start_params.SessionStartParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=SessionStartResponse, + ) + + +class AsyncSessionsResource(AsyncAPIResource): + @cached_property + def with_raw_response(self) -> AsyncSessionsResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/browserbase/stagehand-python#accessing-raw-response-data-eg-headers + """ + return AsyncSessionsResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncSessionsResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/browserbase/stagehand-python#with_streaming_response + """ + return AsyncSessionsResourceWithStreamingResponse(self) + + @overload + async def act( + self, + id: str, + *, + input: session_act_params.Input, + frame_id: str | Omit = omit, + options: session_act_params.Options | Omit = omit, + stream_response: Literal[False] | Omit = omit, + x_language: Literal["typescript", "python", "playground"] | Omit = omit, + x_sdk_version: str | Omit = omit, + x_sent_at: Union[str, datetime] | Omit = omit, + x_stream_response: Literal["true", "false"] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> SessionActResponse: + """ + Executes a browser action using natural language instructions or a predefined + Action object. + + Args: + id: Unique session identifier + + input: Natural language instruction or Action object + + frame_id: Target frame ID for the action + + stream_response: Whether to stream the response via SSE + + x_language: Client SDK language + + x_sdk_version: Version of the Stagehand SDK + + x_sent_at: ISO timestamp when request was sent + + x_stream_response: Whether to stream the response via SSE + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + ... + + @overload + async def act( + self, + id: str, + *, + input: session_act_params.Input, + stream_response: Literal[True], + frame_id: str | Omit = omit, + options: session_act_params.Options | Omit = omit, + x_language: Literal["typescript", "python", "playground"] | Omit = omit, + x_sdk_version: str | Omit = omit, + x_sent_at: Union[str, datetime] | Omit = omit, + x_stream_response: Literal["true", "false"] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> AsyncStream[StreamEvent]: + """ + Executes a browser action using natural language instructions or a predefined + Action object. + + Args: + id: Unique session identifier + + input: Natural language instruction or Action object + + stream_response: Whether to stream the response via SSE + + frame_id: Target frame ID for the action + + x_language: Client SDK language + + x_sdk_version: Version of the Stagehand SDK + + x_sent_at: ISO timestamp when request was sent + + x_stream_response: Whether to stream the response via SSE + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + ... + + @overload + async def act( + self, + id: str, + *, + input: session_act_params.Input, + stream_response: bool, + frame_id: str | Omit = omit, + options: session_act_params.Options | Omit = omit, + x_language: Literal["typescript", "python", "playground"] | Omit = omit, + x_sdk_version: str | Omit = omit, + x_sent_at: Union[str, datetime] | Omit = omit, + x_stream_response: Literal["true", "false"] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> SessionActResponse | AsyncStream[StreamEvent]: + """ + Executes a browser action using natural language instructions or a predefined + Action object. + + Args: + id: Unique session identifier + + input: Natural language instruction or Action object + + stream_response: Whether to stream the response via SSE + + frame_id: Target frame ID for the action + + x_language: Client SDK language + + x_sdk_version: Version of the Stagehand SDK + + x_sent_at: ISO timestamp when request was sent + + x_stream_response: Whether to stream the response via SSE + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + ... + + @required_args(["input"], ["input", "stream_response"]) + async def act( + self, + id: str, + *, + input: session_act_params.Input, + frame_id: str | Omit = omit, + options: session_act_params.Options | Omit = omit, + stream_response: Literal[False] | Literal[True] | Omit = omit, + x_language: Literal["typescript", "python", "playground"] | Omit = omit, + x_sdk_version: str | Omit = omit, + x_sent_at: Union[str, datetime] | Omit = omit, + x_stream_response: Literal["true", "false"] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> SessionActResponse | AsyncStream[StreamEvent]: + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + extra_headers = { + **strip_not_given( + { + "x-language": str(x_language) if is_given(x_language) else not_given, + "x-sdk-version": x_sdk_version, + "x-sent-at": x_sent_at.isoformat() if is_given(x_sent_at) else not_given, + "x-stream-response": str(x_stream_response) if is_given(x_stream_response) else not_given, + } + ), + **(extra_headers or {}), + } + return await self._post( + f"/v1/sessions/{id}/act", + body=await async_maybe_transform( + { + "input": input, + "frame_id": frame_id, + "options": options, + "stream_response": stream_response, + }, + session_act_params.SessionActParamsStreaming + if stream_response + else session_act_params.SessionActParamsNonStreaming, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=SessionActResponse, + stream=stream_response or False, + stream_cls=AsyncStream[StreamEvent], + ) + + async def end( + self, + id: str, + *, + x_language: Literal["typescript", "python", "playground"] | Omit = omit, + x_sdk_version: str | Omit = omit, + x_sent_at: Union[str, datetime] | Omit = omit, + x_stream_response: Literal["true", "false"] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> SessionEndResponse: + """ + Terminates the browser session and releases all associated resources. + + Args: + id: Unique session identifier + + x_language: Client SDK language + + x_sdk_version: Version of the Stagehand SDK + + x_sent_at: ISO timestamp when request was sent + + x_stream_response: Whether to stream the response via SSE + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + extra_headers = { + **strip_not_given( + { + "x-language": str(x_language) if is_given(x_language) else not_given, + "x-sdk-version": x_sdk_version, + "x-sent-at": x_sent_at.isoformat() if is_given(x_sent_at) else not_given, + "x-stream-response": str(x_stream_response) if is_given(x_stream_response) else not_given, + } + ), + **(extra_headers or {}), + } + return await self._post( + f"/v1/sessions/{id}/end", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=SessionEndResponse, + ) + + @overload + async def execute( + self, + id: str, + *, + agent_config: session_execute_params.AgentConfig, + execute_options: session_execute_params.ExecuteOptions, + frame_id: str | Omit = omit, + stream_response: Literal[False] | Omit = omit, + x_language: Literal["typescript", "python", "playground"] | Omit = omit, + x_sdk_version: str | Omit = omit, + x_sent_at: Union[str, datetime] | Omit = omit, + x_stream_response: Literal["true", "false"] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> SessionExecuteResponse: + """ + Runs an autonomous AI agent that can perform complex multi-step browser tasks. + + Args: + id: Unique session identifier + + frame_id: Target frame ID for the agent + + stream_response: Whether to stream the response via SSE + + x_language: Client SDK language + + x_sdk_version: Version of the Stagehand SDK + + x_sent_at: ISO timestamp when request was sent + + x_stream_response: Whether to stream the response via SSE + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + ... + + @overload + async def execute( + self, + id: str, + *, + agent_config: session_execute_params.AgentConfig, + execute_options: session_execute_params.ExecuteOptions, + stream_response: Literal[True], + frame_id: str | Omit = omit, + x_language: Literal["typescript", "python", "playground"] | Omit = omit, + x_sdk_version: str | Omit = omit, + x_sent_at: Union[str, datetime] | Omit = omit, + x_stream_response: Literal["true", "false"] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> AsyncStream[StreamEvent]: + """ + Runs an autonomous AI agent that can perform complex multi-step browser tasks. + + Args: + id: Unique session identifier + + stream_response: Whether to stream the response via SSE + + frame_id: Target frame ID for the agent + + x_language: Client SDK language + + x_sdk_version: Version of the Stagehand SDK + + x_sent_at: ISO timestamp when request was sent + + x_stream_response: Whether to stream the response via SSE + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request - For more information, see https://www.github.com/browserbase/stagehand-python#with_streaming_response + timeout: Override the client-level default timeout for this request, in seconds """ - return AsyncSessionsResourceWithStreamingResponse(self) + ... - async def act( + @overload + async def execute( self, id: str, *, - input: session_act_params.Input, + agent_config: session_execute_params.AgentConfig, + execute_options: session_execute_params.ExecuteOptions, + stream_response: bool, frame_id: str | Omit = omit, - options: session_act_params.Options | Omit = omit, x_language: Literal["typescript", "python", "playground"] | Omit = omit, x_sdk_version: str | Omit = omit, x_sent_at: Union[str, datetime] | Omit = omit, @@ -621,17 +1519,16 @@ async def act( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> SessionActResponse: + ) -> SessionExecuteResponse | AsyncStream[StreamEvent]: """ - Executes a browser action using natural language instructions or a predefined - Action object. + Runs an autonomous AI agent that can perform complex multi-step browser tasks. Args: id: Unique session identifier - input: Natural language instruction or Action object + stream_response: Whether to stream the response via SSE - frame_id: Target frame ID for the action + frame_id: Target frame ID for the agent x_language: Client SDK language @@ -649,6 +1546,28 @@ async def act( timeout: Override the client-level default timeout for this request, in seconds """ + ... + + @required_args(["agent_config", "execute_options"], ["agent_config", "execute_options", "stream_response"]) + async def execute( + self, + id: str, + *, + agent_config: session_execute_params.AgentConfig, + execute_options: session_execute_params.ExecuteOptions, + frame_id: str | Omit = omit, + stream_response: Literal[False] | Literal[True] | Omit = omit, + x_language: Literal["typescript", "python", "playground"] | Omit = omit, + x_sdk_version: str | Omit = omit, + x_sent_at: Union[str, datetime] | Omit = omit, + x_stream_response: Literal["true", "false"] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> SessionExecuteResponse | AsyncStream[StreamEvent]: if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") extra_headers = { @@ -663,25 +1582,36 @@ async def act( **(extra_headers or {}), } return await self._post( - f"/v1/sessions/{id}/act", + f"/v1/sessions/{id}/agentExecute", body=await async_maybe_transform( { - "input": input, + "agent_config": agent_config, + "execute_options": execute_options, "frame_id": frame_id, - "options": options, + "stream_response": stream_response, }, - session_act_params.SessionActParams, + session_execute_params.SessionExecuteParamsStreaming + if stream_response + else session_execute_params.SessionExecuteParamsNonStreaming, ), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=SessionActResponse, + cast_to=SessionExecuteResponse, + stream=stream_response or False, + stream_cls=AsyncStream[StreamEvent], ) - async def end( + @overload + async def extract( self, id: str, *, + frame_id: str | Omit = omit, + instruction: str | Omit = omit, + options: session_extract_params.Options | Omit = omit, + schema: Dict[str, object] | Omit = omit, + stream_response: Literal[False] | Omit = omit, x_language: Literal["typescript", "python", "playground"] | Omit = omit, x_sdk_version: str | Omit = omit, x_sent_at: Union[str, datetime] | Omit = omit, @@ -692,13 +1622,21 @@ async def end( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> SessionEndResponse: + ) -> SessionExtractResponse: """ - Terminates the browser session and releases all associated resources. + Extracts structured data from the current page using AI-powered analysis. Args: id: Unique session identifier + frame_id: Target frame ID for the extraction + + instruction: Natural language instruction for what to extract + + schema: JSON Schema defining the structure of data to extract + + stream_response: Whether to stream the response via SSE + x_language: Client SDK language x_sdk_version: Version of the Stagehand SDK @@ -715,34 +1653,18 @@ async def end( timeout: Override the client-level default timeout for this request, in seconds """ - if not id: - raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") - extra_headers = { - **strip_not_given( - { - "x-language": str(x_language) if is_given(x_language) else not_given, - "x-sdk-version": x_sdk_version, - "x-sent-at": x_sent_at.isoformat() if is_given(x_sent_at) else not_given, - "x-stream-response": str(x_stream_response) if is_given(x_stream_response) else not_given, - } - ), - **(extra_headers or {}), - } - return await self._post( - f"/v1/sessions/{id}/end", - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=SessionEndResponse, - ) + ... - async def execute( + @overload + async def extract( self, id: str, *, - agent_config: session_execute_params.AgentConfig, - execute_options: session_execute_params.ExecuteOptions, + stream_response: Literal[True], frame_id: str | Omit = omit, + instruction: str | Omit = omit, + options: session_extract_params.Options | Omit = omit, + schema: Dict[str, object] | Omit = omit, x_language: Literal["typescript", "python", "playground"] | Omit = omit, x_sdk_version: str | Omit = omit, x_sent_at: Union[str, datetime] | Omit = omit, @@ -753,14 +1675,20 @@ async def execute( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> SessionExecuteResponse: + ) -> AsyncStream[StreamEvent]: """ - Runs an autonomous AI agent that can perform complex multi-step browser tasks. + Extracts structured data from the current page using AI-powered analysis. Args: id: Unique session identifier - frame_id: Target frame ID for the agent + stream_response: Whether to stream the response via SSE + + frame_id: Target frame ID for the extraction + + instruction: Natural language instruction for what to extract + + schema: JSON Schema defining the structure of data to extract x_language: Client SDK language @@ -778,39 +1706,14 @@ async def execute( timeout: Override the client-level default timeout for this request, in seconds """ - if not id: - raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") - extra_headers = { - **strip_not_given( - { - "x-language": str(x_language) if is_given(x_language) else not_given, - "x-sdk-version": x_sdk_version, - "x-sent-at": x_sent_at.isoformat() if is_given(x_sent_at) else not_given, - "x-stream-response": str(x_stream_response) if is_given(x_stream_response) else not_given, - } - ), - **(extra_headers or {}), - } - return await self._post( - f"/v1/sessions/{id}/agentExecute", - body=await async_maybe_transform( - { - "agent_config": agent_config, - "execute_options": execute_options, - "frame_id": frame_id, - }, - session_execute_params.SessionExecuteParams, - ), - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=SessionExecuteResponse, - ) + ... + @overload async def extract( self, id: str, *, + stream_response: bool, frame_id: str | Omit = omit, instruction: str | Omit = omit, options: session_extract_params.Options | Omit = omit, @@ -825,13 +1728,15 @@ async def extract( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> SessionExtractResponse: + ) -> SessionExtractResponse | AsyncStream[StreamEvent]: """ Extracts structured data from the current page using AI-powered analysis. Args: id: Unique session identifier + stream_response: Whether to stream the response via SSE + frame_id: Target frame ID for the extraction instruction: Natural language instruction for what to extract @@ -854,6 +1759,28 @@ async def extract( timeout: Override the client-level default timeout for this request, in seconds """ + ... + + async def extract( + self, + id: str, + *, + frame_id: str | Omit = omit, + instruction: str | Omit = omit, + options: session_extract_params.Options | Omit = omit, + schema: Dict[str, object] | Omit = omit, + stream_response: Literal[False] | Literal[True] | Omit = omit, + x_language: Literal["typescript", "python", "playground"] | Omit = omit, + x_sdk_version: str | Omit = omit, + x_sent_at: Union[str, datetime] | Omit = omit, + x_stream_response: Literal["true", "false"] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> SessionExtractResponse | AsyncStream[StreamEvent]: if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") extra_headers = { @@ -875,13 +1802,18 @@ async def extract( "instruction": instruction, "options": options, "schema": schema, + "stream_response": stream_response, }, - session_extract_params.SessionExtractParams, + session_extract_params.SessionExtractParamsStreaming + if stream_response + else session_extract_params.SessionExtractParamsNonStreaming, ), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), cast_to=SessionExtractResponse, + stream=stream_response or False, + stream_cls=AsyncStream[StreamEvent], ) async def navigate( @@ -891,6 +1823,7 @@ async def navigate( url: str, frame_id: str | Omit = omit, options: session_navigate_params.Options | Omit = omit, + stream_response: bool | Omit = omit, x_language: Literal["typescript", "python", "playground"] | Omit = omit, x_sdk_version: str | Omit = omit, x_sent_at: Union[str, datetime] | Omit = omit, @@ -912,6 +1845,8 @@ async def navigate( frame_id: Target frame ID for the navigation + stream_response: Whether to stream the response via SSE + x_language: Client SDK language x_sdk_version: Version of the Stagehand SDK @@ -948,6 +1883,7 @@ async def navigate( "url": url, "frame_id": frame_id, "options": options, + "stream_response": stream_response, }, session_navigate_params.SessionNavigateParams, ), @@ -957,6 +1893,7 @@ async def navigate( cast_to=SessionNavigateResponse, ) + @overload async def observe( self, id: str, @@ -964,6 +1901,7 @@ async def observe( frame_id: str | Omit = omit, instruction: str | Omit = omit, options: session_observe_params.Options | Omit = omit, + stream_response: Literal[False] | Omit = omit, x_language: Literal["typescript", "python", "playground"] | Omit = omit, x_sdk_version: str | Omit = omit, x_sent_at: Union[str, datetime] | Omit = omit, @@ -986,6 +1924,110 @@ async def observe( instruction: Natural language instruction for what actions to find + stream_response: Whether to stream the response via SSE + + x_language: Client SDK language + + x_sdk_version: Version of the Stagehand SDK + + x_sent_at: ISO timestamp when request was sent + + x_stream_response: Whether to stream the response via SSE + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + ... + + @overload + async def observe( + self, + id: str, + *, + stream_response: Literal[True], + frame_id: str | Omit = omit, + instruction: str | Omit = omit, + options: session_observe_params.Options | Omit = omit, + x_language: Literal["typescript", "python", "playground"] | Omit = omit, + x_sdk_version: str | Omit = omit, + x_sent_at: Union[str, datetime] | Omit = omit, + x_stream_response: Literal["true", "false"] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> AsyncStream[StreamEvent]: + """ + Identifies and returns available actions on the current page that match the + given instruction. + + Args: + id: Unique session identifier + + stream_response: Whether to stream the response via SSE + + frame_id: Target frame ID for the observation + + instruction: Natural language instruction for what actions to find + + x_language: Client SDK language + + x_sdk_version: Version of the Stagehand SDK + + x_sent_at: ISO timestamp when request was sent + + x_stream_response: Whether to stream the response via SSE + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + ... + + @overload + async def observe( + self, + id: str, + *, + stream_response: bool, + frame_id: str | Omit = omit, + instruction: str | Omit = omit, + options: session_observe_params.Options | Omit = omit, + x_language: Literal["typescript", "python", "playground"] | Omit = omit, + x_sdk_version: str | Omit = omit, + x_sent_at: Union[str, datetime] | Omit = omit, + x_stream_response: Literal["true", "false"] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> SessionObserveResponse | AsyncStream[StreamEvent]: + """ + Identifies and returns available actions on the current page that match the + given instruction. + + Args: + id: Unique session identifier + + stream_response: Whether to stream the response via SSE + + frame_id: Target frame ID for the observation + + instruction: Natural language instruction for what actions to find + x_language: Client SDK language x_sdk_version: Version of the Stagehand SDK @@ -1002,6 +2044,27 @@ async def observe( timeout: Override the client-level default timeout for this request, in seconds """ + ... + + async def observe( + self, + id: str, + *, + frame_id: str | Omit = omit, + instruction: str | Omit = omit, + options: session_observe_params.Options | Omit = omit, + stream_response: Literal[False] | Literal[True] | Omit = omit, + x_language: Literal["typescript", "python", "playground"] | Omit = omit, + x_sdk_version: str | Omit = omit, + x_sent_at: Union[str, datetime] | Omit = omit, + x_stream_response: Literal["true", "false"] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> SessionObserveResponse | AsyncStream[StreamEvent]: if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") extra_headers = { @@ -1022,13 +2085,18 @@ async def observe( "frame_id": frame_id, "instruction": instruction, "options": options, + "stream_response": stream_response, }, - session_observe_params.SessionObserveParams, + session_observe_params.SessionObserveParamsStreaming + if stream_response + else session_observe_params.SessionObserveParamsNonStreaming, ), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), cast_to=SessionObserveResponse, + stream=stream_response or False, + stream_cls=AsyncStream[StreamEvent], ) async def start( diff --git a/src/stagehand/types/__init__.py b/src/stagehand/types/__init__.py index 68575911..890eccb0 100644 --- a/src/stagehand/types/__init__.py +++ b/src/stagehand/types/__init__.py @@ -3,6 +3,8 @@ from __future__ import annotations from .action import Action as Action +from .action_param import ActionParam as ActionParam +from .stream_event import StreamEvent as StreamEvent from .model_config_param import ModelConfigParam as ModelConfigParam from .session_act_params import SessionActParams as SessionActParams from .session_act_response import SessionActResponse as SessionActResponse diff --git a/src/stagehand/types/action_param.py b/src/stagehand/types/action_param.py new file mode 100644 index 00000000..6ef78c2d --- /dev/null +++ b/src/stagehand/types/action_param.py @@ -0,0 +1,25 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Required, TypedDict + +from .._types import SequenceNotStr + +__all__ = ["ActionParam"] + + +class ActionParam(TypedDict, total=False): + """Action object returned by observe and used by act""" + + description: Required[str] + """Human-readable description of the action""" + + selector: Required[str] + """CSS selector or XPath for the element""" + + arguments: SequenceNotStr[str] + """Arguments to pass to the method""" + + method: str + """The method to execute (click, fill, etc.)""" diff --git a/src/stagehand/types/session_act_params.py b/src/stagehand/types/session_act_params.py index 67c04481..153ac6c8 100644 --- a/src/stagehand/types/session_act_params.py +++ b/src/stagehand/types/session_act_params.py @@ -6,14 +6,14 @@ from datetime import datetime from typing_extensions import Literal, Required, Annotated, TypeAlias, TypedDict -from .._types import SequenceNotStr from .._utils import PropertyInfo +from .action_param import ActionParam from .model_config_param import ModelConfigParam -__all__ = ["SessionActParams", "Input", "InputActionInput", "Options"] +__all__ = ["SessionActParamsBase", "Input", "Options", "SessionActParamsNonStreaming", "SessionActParamsStreaming"] -class SessionActParams(TypedDict, total=False): +class SessionActParamsBase(TypedDict, total=False): input: Required[Input] """Natural language instruction or Action object""" @@ -35,23 +35,7 @@ class SessionActParams(TypedDict, total=False): """Whether to stream the response via SSE""" -class InputActionInput(TypedDict, total=False): - """Action object returned by observe and used by act""" - - description: Required[str] - """Human-readable description of the action""" - - selector: Required[str] - """CSS selector or XPath for the element""" - - arguments: SequenceNotStr[str] - """Arguments to pass to the method""" - - method: str - """The method to execute (click, fill, etc.)""" - - -Input: TypeAlias = Union[str, InputActionInput] +Input: TypeAlias = Union[str, ActionParam] class Options(TypedDict, total=False): @@ -66,3 +50,16 @@ class Options(TypedDict, total=False): variables: Dict[str, str] """Variables to substitute in the action instruction""" + + +class SessionActParamsNonStreaming(SessionActParamsBase, total=False): + stream_response: Annotated[Literal[False], PropertyInfo(alias="streamResponse")] + """Whether to stream the response via SSE""" + + +class SessionActParamsStreaming(SessionActParamsBase): + stream_response: Required[Annotated[Literal[True], PropertyInfo(alias="streamResponse")]] + """Whether to stream the response via SSE""" + + +SessionActParams = Union[SessionActParamsNonStreaming, SessionActParamsStreaming] diff --git a/src/stagehand/types/session_execute_params.py b/src/stagehand/types/session_execute_params.py index 458f85bc..2957d8e8 100644 --- a/src/stagehand/types/session_execute_params.py +++ b/src/stagehand/types/session_execute_params.py @@ -9,10 +9,16 @@ from .._utils import PropertyInfo from .model_config_param import ModelConfigParam -__all__ = ["SessionExecuteParams", "AgentConfig", "ExecuteOptions"] +__all__ = [ + "SessionExecuteParamsBase", + "AgentConfig", + "ExecuteOptions", + "SessionExecuteParamsNonStreaming", + "SessionExecuteParamsStreaming", +] -class SessionExecuteParams(TypedDict, total=False): +class SessionExecuteParamsBase(TypedDict, total=False): agent_config: Required[Annotated[AgentConfig, PropertyInfo(alias="agentConfig")]] execute_options: Required[Annotated[ExecuteOptions, PropertyInfo(alias="executeOptions")]] @@ -56,3 +62,16 @@ class ExecuteOptions(TypedDict, total=False): max_steps: Annotated[float, PropertyInfo(alias="maxSteps")] """Maximum number of steps the agent can take""" + + +class SessionExecuteParamsNonStreaming(SessionExecuteParamsBase, total=False): + stream_response: Annotated[Literal[False], PropertyInfo(alias="streamResponse")] + """Whether to stream the response via SSE""" + + +class SessionExecuteParamsStreaming(SessionExecuteParamsBase): + stream_response: Required[Annotated[Literal[True], PropertyInfo(alias="streamResponse")]] + """Whether to stream the response via SSE""" + + +SessionExecuteParams = Union[SessionExecuteParamsNonStreaming, SessionExecuteParamsStreaming] diff --git a/src/stagehand/types/session_extract_params.py b/src/stagehand/types/session_extract_params.py index a62e3eb8..c18800f8 100644 --- a/src/stagehand/types/session_extract_params.py +++ b/src/stagehand/types/session_extract_params.py @@ -4,15 +4,15 @@ from typing import Dict, Union from datetime import datetime -from typing_extensions import Literal, Annotated, TypedDict +from typing_extensions import Literal, Required, Annotated, TypedDict from .._utils import PropertyInfo from .model_config_param import ModelConfigParam -__all__ = ["SessionExtractParams", "Options"] +__all__ = ["SessionExtractParamsBase", "Options", "SessionExtractParamsNonStreaming", "SessionExtractParamsStreaming"] -class SessionExtractParams(TypedDict, total=False): +class SessionExtractParamsBase(TypedDict, total=False): frame_id: Annotated[str, PropertyInfo(alias="frameId")] """Target frame ID for the extraction""" @@ -49,3 +49,16 @@ class Options(TypedDict, total=False): timeout: float """Timeout in ms for the extraction""" + + +class SessionExtractParamsNonStreaming(SessionExtractParamsBase, total=False): + stream_response: Annotated[Literal[False], PropertyInfo(alias="streamResponse")] + """Whether to stream the response via SSE""" + + +class SessionExtractParamsStreaming(SessionExtractParamsBase): + stream_response: Required[Annotated[Literal[True], PropertyInfo(alias="streamResponse")]] + """Whether to stream the response via SSE""" + + +SessionExtractParams = Union[SessionExtractParamsNonStreaming, SessionExtractParamsStreaming] diff --git a/src/stagehand/types/session_navigate_params.py b/src/stagehand/types/session_navigate_params.py index 15205f06..59a86eae 100644 --- a/src/stagehand/types/session_navigate_params.py +++ b/src/stagehand/types/session_navigate_params.py @@ -20,6 +20,9 @@ class SessionNavigateParams(TypedDict, total=False): options: Options + stream_response: Annotated[bool, PropertyInfo(alias="streamResponse")] + """Whether to stream the response via SSE""" + x_language: Annotated[Literal["typescript", "python", "playground"], PropertyInfo(alias="x-language")] """Client SDK language""" diff --git a/src/stagehand/types/session_observe_params.py b/src/stagehand/types/session_observe_params.py index 089db6b1..9d18b185 100644 --- a/src/stagehand/types/session_observe_params.py +++ b/src/stagehand/types/session_observe_params.py @@ -4,15 +4,15 @@ from typing import Union from datetime import datetime -from typing_extensions import Literal, Annotated, TypedDict +from typing_extensions import Literal, Required, Annotated, TypedDict from .._utils import PropertyInfo from .model_config_param import ModelConfigParam -__all__ = ["SessionObserveParams", "Options"] +__all__ = ["SessionObserveParamsBase", "Options", "SessionObserveParamsNonStreaming", "SessionObserveParamsStreaming"] -class SessionObserveParams(TypedDict, total=False): +class SessionObserveParamsBase(TypedDict, total=False): frame_id: Annotated[str, PropertyInfo(alias="frameId")] """Target frame ID for the observation""" @@ -46,3 +46,16 @@ class Options(TypedDict, total=False): timeout: float """Timeout in ms for the observation""" + + +class SessionObserveParamsNonStreaming(SessionObserveParamsBase, total=False): + stream_response: Annotated[Literal[False], PropertyInfo(alias="streamResponse")] + """Whether to stream the response via SSE""" + + +class SessionObserveParamsStreaming(SessionObserveParamsBase): + stream_response: Required[Annotated[Literal[True], PropertyInfo(alias="streamResponse")]] + """Whether to stream the response via SSE""" + + +SessionObserveParams = Union[SessionObserveParamsNonStreaming, SessionObserveParamsStreaming] diff --git a/src/stagehand/types/session_start_response.py b/src/stagehand/types/session_start_response.py index c8e74c36..1bec1267 100644 --- a/src/stagehand/types/session_start_response.py +++ b/src/stagehand/types/session_start_response.py @@ -10,8 +10,11 @@ class Data(BaseModel): available: bool + connect_url: str = FieldInfo(alias="connectUrl") + """CDP WebSocket URL for connecting to the Browserbase cloud browser""" + session_id: str = FieldInfo(alias="sessionId") - """Unique session identifier""" + """Unique Browserbase session identifier""" class SessionStartResponse(BaseModel): diff --git a/src/stagehand/types/stream_event.py b/src/stagehand/types/stream_event.py new file mode 100644 index 00000000..2504a1d1 --- /dev/null +++ b/src/stagehand/types/stream_event.py @@ -0,0 +1,44 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Union, Optional +from typing_extensions import Literal, TypeAlias + +from .._models import BaseModel + +__all__ = ["StreamEvent", "Data", "DataStreamEventSystemDataOutput", "DataStreamEventLogDataOutput"] + + +class DataStreamEventSystemDataOutput(BaseModel): + status: Literal["starting", "connected", "running", "finished", "error"] + """Current status of the streaming operation""" + + error: Optional[str] = None + """Error message (present when status is 'error')""" + + result: Optional[object] = None + """Operation result (present when status is 'finished')""" + + +class DataStreamEventLogDataOutput(BaseModel): + message: str + """Log message from the operation""" + + status: Literal["running"] + + +Data: TypeAlias = Union[DataStreamEventSystemDataOutput, DataStreamEventLogDataOutput] + + +class StreamEvent(BaseModel): + """Server-Sent Event emitted during streaming responses. + + Events are sent as `data: \n\n`. + """ + + id: str + """Unique identifier for this event""" + + data: Data + + type: Literal["system", "log"] + """Type of stream event - system events or log messages""" diff --git a/tests/api_resources/test_sessions.py b/tests/api_resources/test_sessions.py index 1874ca18..84503a8a 100644 --- a/tests/api_resources/test_sessions.py +++ b/tests/api_resources/test_sessions.py @@ -28,7 +28,7 @@ class TestSessions: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - def test_method_act(self, client: Stagehand) -> None: + def test_method_act_overload_1(self, client: Stagehand) -> None: session = client.sessions.act( id="c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123", input="Click the login button", @@ -37,7 +37,7 @@ def test_method_act(self, client: Stagehand) -> None: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - def test_method_act_with_all_params(self, client: Stagehand) -> None: + def test_method_act_with_all_params_overload_1(self, client: Stagehand) -> None: session = client.sessions.act( id="c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123", input="Click the login button", @@ -47,6 +47,7 @@ def test_method_act_with_all_params(self, client: Stagehand) -> None: "timeout": 30000, "variables": {"username": "john_doe"}, }, + stream_response=False, x_language="typescript", x_sdk_version="3.0.6", x_sent_at=parse_datetime("2025-01-15T10:30:00Z"), @@ -56,7 +57,7 @@ def test_method_act_with_all_params(self, client: Stagehand) -> None: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - def test_raw_response_act(self, client: Stagehand) -> None: + def test_raw_response_act_overload_1(self, client: Stagehand) -> None: response = client.sessions.with_raw_response.act( id="c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123", input="Click the login button", @@ -69,7 +70,7 @@ def test_raw_response_act(self, client: Stagehand) -> None: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - def test_streaming_response_act(self, client: Stagehand) -> None: + def test_streaming_response_act_overload_1(self, client: Stagehand) -> None: with client.sessions.with_streaming_response.act( id="c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123", input="Click the login button", @@ -84,13 +85,82 @@ def test_streaming_response_act(self, client: Stagehand) -> None: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - def test_path_params_act(self, client: Stagehand) -> None: + def test_path_params_act_overload_1(self, client: Stagehand) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): client.sessions.with_raw_response.act( id="", input="Click the login button", ) + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_act_overload_2(self, client: Stagehand) -> None: + session_stream = client.sessions.act( + id="c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123", + input="Click the login button", + stream_response=True, + ) + session_stream.response.close() + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_act_with_all_params_overload_2(self, client: Stagehand) -> None: + session_stream = client.sessions.act( + id="c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123", + input="Click the login button", + stream_response=True, + frame_id="frameId", + options={ + "model": "openai/gpt-5-nano", + "timeout": 30000, + "variables": {"username": "john_doe"}, + }, + x_language="typescript", + x_sdk_version="3.0.6", + x_sent_at=parse_datetime("2025-01-15T10:30:00Z"), + x_stream_response="true", + ) + session_stream.response.close() + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_raw_response_act_overload_2(self, client: Stagehand) -> None: + response = client.sessions.with_raw_response.act( + id="c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123", + input="Click the login button", + stream_response=True, + ) + + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + stream = response.parse() + stream.close() + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_streaming_response_act_overload_2(self, client: Stagehand) -> None: + with client.sessions.with_streaming_response.act( + id="c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123", + input="Click the login button", + stream_response=True, + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + stream = response.parse() + stream.close() + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_path_params_act_overload_2(self, client: Stagehand) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.sessions.with_raw_response.act( + id="", + input="Click the login button", + stream_response=True, + ) + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_method_end(self, client: Stagehand) -> None: @@ -147,7 +217,7 @@ def test_path_params_end(self, client: Stagehand) -> None: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - def test_method_execute(self, client: Stagehand) -> None: + def test_method_execute_overload_1(self, client: Stagehand) -> None: session = client.sessions.execute( id="c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123", agent_config={}, @@ -159,7 +229,7 @@ def test_method_execute(self, client: Stagehand) -> None: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - def test_method_execute_with_all_params(self, client: Stagehand) -> None: + def test_method_execute_with_all_params_overload_1(self, client: Stagehand) -> None: session = client.sessions.execute( id="c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123", agent_config={ @@ -173,6 +243,7 @@ def test_method_execute_with_all_params(self, client: Stagehand) -> None: "max_steps": 20, }, frame_id="frameId", + stream_response=False, x_language="typescript", x_sdk_version="3.0.6", x_sent_at=parse_datetime("2025-01-15T10:30:00Z"), @@ -182,7 +253,7 @@ def test_method_execute_with_all_params(self, client: Stagehand) -> None: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - def test_raw_response_execute(self, client: Stagehand) -> None: + def test_raw_response_execute_overload_1(self, client: Stagehand) -> None: response = client.sessions.with_raw_response.execute( id="c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123", agent_config={}, @@ -198,7 +269,7 @@ def test_raw_response_execute(self, client: Stagehand) -> None: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - def test_streaming_response_execute(self, client: Stagehand) -> None: + def test_streaming_response_execute_overload_1(self, client: Stagehand) -> None: with client.sessions.with_streaming_response.execute( id="c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123", agent_config={}, @@ -216,7 +287,7 @@ def test_streaming_response_execute(self, client: Stagehand) -> None: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - def test_path_params_execute(self, client: Stagehand) -> None: + def test_path_params_execute_overload_1(self, client: Stagehand) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): client.sessions.with_raw_response.execute( id="", @@ -228,7 +299,92 @@ def test_path_params_execute(self, client: Stagehand) -> None: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - def test_method_extract(self, client: Stagehand) -> None: + def test_method_execute_overload_2(self, client: Stagehand) -> None: + session_stream = client.sessions.execute( + id="c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123", + agent_config={}, + execute_options={ + "instruction": "Log in with username 'demo' and password 'test123', then navigate to settings" + }, + stream_response=True, + ) + session_stream.response.close() + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_execute_with_all_params_overload_2(self, client: Stagehand) -> None: + session_stream = client.sessions.execute( + id="c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123", + agent_config={ + "cua": True, + "model": "openai/gpt-5-nano", + "system_prompt": "systemPrompt", + }, + execute_options={ + "instruction": "Log in with username 'demo' and password 'test123', then navigate to settings", + "highlight_cursor": True, + "max_steps": 20, + }, + stream_response=True, + frame_id="frameId", + x_language="typescript", + x_sdk_version="3.0.6", + x_sent_at=parse_datetime("2025-01-15T10:30:00Z"), + x_stream_response="true", + ) + session_stream.response.close() + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_raw_response_execute_overload_2(self, client: Stagehand) -> None: + response = client.sessions.with_raw_response.execute( + id="c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123", + agent_config={}, + execute_options={ + "instruction": "Log in with username 'demo' and password 'test123', then navigate to settings" + }, + stream_response=True, + ) + + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + stream = response.parse() + stream.close() + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_streaming_response_execute_overload_2(self, client: Stagehand) -> None: + with client.sessions.with_streaming_response.execute( + id="c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123", + agent_config={}, + execute_options={ + "instruction": "Log in with username 'demo' and password 'test123', then navigate to settings" + }, + stream_response=True, + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + stream = response.parse() + stream.close() + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_path_params_execute_overload_2(self, client: Stagehand) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.sessions.with_raw_response.execute( + id="", + agent_config={}, + execute_options={ + "instruction": "Log in with username 'demo' and password 'test123', then navigate to settings" + }, + stream_response=True, + ) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_extract_overload_1(self, client: Stagehand) -> None: session = client.sessions.extract( id="c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123", ) @@ -236,7 +392,7 @@ def test_method_extract(self, client: Stagehand) -> None: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - def test_method_extract_with_all_params(self, client: Stagehand) -> None: + def test_method_extract_with_all_params_overload_1(self, client: Stagehand) -> None: session = client.sessions.extract( id="c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123", frame_id="frameId", @@ -247,6 +403,7 @@ def test_method_extract_with_all_params(self, client: Stagehand) -> None: "timeout": 30000, }, schema={"foo": "bar"}, + stream_response=False, x_language="typescript", x_sdk_version="3.0.6", x_sent_at=parse_datetime("2025-01-15T10:30:00Z"), @@ -256,7 +413,7 @@ def test_method_extract_with_all_params(self, client: Stagehand) -> None: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - def test_raw_response_extract(self, client: Stagehand) -> None: + def test_raw_response_extract_overload_1(self, client: Stagehand) -> None: response = client.sessions.with_raw_response.extract( id="c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123", ) @@ -268,7 +425,7 @@ def test_raw_response_extract(self, client: Stagehand) -> None: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - def test_streaming_response_extract(self, client: Stagehand) -> None: + def test_streaming_response_extract_overload_1(self, client: Stagehand) -> None: with client.sessions.with_streaming_response.extract( id="c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123", ) as response: @@ -282,10 +439,76 @@ def test_streaming_response_extract(self, client: Stagehand) -> None: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - def test_path_params_extract(self, client: Stagehand) -> None: + def test_path_params_extract_overload_1(self, client: Stagehand) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.sessions.with_raw_response.extract( + id="", + ) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_extract_overload_2(self, client: Stagehand) -> None: + session_stream = client.sessions.extract( + id="c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123", + stream_response=True, + ) + session_stream.response.close() + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_extract_with_all_params_overload_2(self, client: Stagehand) -> None: + session_stream = client.sessions.extract( + id="c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123", + stream_response=True, + frame_id="frameId", + instruction="Extract all product names and prices from the page", + options={ + "model": "openai/gpt-5-nano", + "selector": "#main-content", + "timeout": 30000, + }, + schema={"foo": "bar"}, + x_language="typescript", + x_sdk_version="3.0.6", + x_sent_at=parse_datetime("2025-01-15T10:30:00Z"), + x_stream_response="true", + ) + session_stream.response.close() + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_raw_response_extract_overload_2(self, client: Stagehand) -> None: + response = client.sessions.with_raw_response.extract( + id="c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123", + stream_response=True, + ) + + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + stream = response.parse() + stream.close() + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_streaming_response_extract_overload_2(self, client: Stagehand) -> None: + with client.sessions.with_streaming_response.extract( + id="c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123", + stream_response=True, + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + stream = response.parse() + stream.close() + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_path_params_extract_overload_2(self, client: Stagehand) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): client.sessions.with_raw_response.extract( id="", + stream_response=True, ) @pytest.mark.skip(reason="Prism tests are disabled") @@ -309,6 +532,7 @@ def test_method_navigate_with_all_params(self, client: Stagehand) -> None: "timeout": 30000, "wait_until": "networkidle", }, + stream_response=True, x_language="typescript", x_sdk_version="3.0.6", x_sent_at=parse_datetime("2025-01-15T10:30:00Z"), @@ -355,7 +579,7 @@ def test_path_params_navigate(self, client: Stagehand) -> None: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - def test_method_observe(self, client: Stagehand) -> None: + def test_method_observe_overload_1(self, client: Stagehand) -> None: session = client.sessions.observe( id="c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123", ) @@ -363,7 +587,7 @@ def test_method_observe(self, client: Stagehand) -> None: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - def test_method_observe_with_all_params(self, client: Stagehand) -> None: + def test_method_observe_with_all_params_overload_1(self, client: Stagehand) -> None: session = client.sessions.observe( id="c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123", frame_id="frameId", @@ -373,6 +597,7 @@ def test_method_observe_with_all_params(self, client: Stagehand) -> None: "selector": "nav", "timeout": 30000, }, + stream_response=False, x_language="typescript", x_sdk_version="3.0.6", x_sent_at=parse_datetime("2025-01-15T10:30:00Z"), @@ -382,7 +607,7 @@ def test_method_observe_with_all_params(self, client: Stagehand) -> None: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - def test_raw_response_observe(self, client: Stagehand) -> None: + def test_raw_response_observe_overload_1(self, client: Stagehand) -> None: response = client.sessions.with_raw_response.observe( id="c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123", ) @@ -394,7 +619,7 @@ def test_raw_response_observe(self, client: Stagehand) -> None: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - def test_streaming_response_observe(self, client: Stagehand) -> None: + def test_streaming_response_observe_overload_1(self, client: Stagehand) -> None: with client.sessions.with_streaming_response.observe( id="c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123", ) as response: @@ -408,10 +633,75 @@ def test_streaming_response_observe(self, client: Stagehand) -> None: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - def test_path_params_observe(self, client: Stagehand) -> None: + def test_path_params_observe_overload_1(self, client: Stagehand) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.sessions.with_raw_response.observe( + id="", + ) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_observe_overload_2(self, client: Stagehand) -> None: + session_stream = client.sessions.observe( + id="c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123", + stream_response=True, + ) + session_stream.response.close() + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_observe_with_all_params_overload_2(self, client: Stagehand) -> None: + session_stream = client.sessions.observe( + id="c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123", + stream_response=True, + frame_id="frameId", + instruction="Find all clickable navigation links", + options={ + "model": "openai/gpt-5-nano", + "selector": "nav", + "timeout": 30000, + }, + x_language="typescript", + x_sdk_version="3.0.6", + x_sent_at=parse_datetime("2025-01-15T10:30:00Z"), + x_stream_response="true", + ) + session_stream.response.close() + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_raw_response_observe_overload_2(self, client: Stagehand) -> None: + response = client.sessions.with_raw_response.observe( + id="c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123", + stream_response=True, + ) + + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + stream = response.parse() + stream.close() + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_streaming_response_observe_overload_2(self, client: Stagehand) -> None: + with client.sessions.with_streaming_response.observe( + id="c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123", + stream_response=True, + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + stream = response.parse() + stream.close() + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_path_params_observe_overload_2(self, client: Stagehand) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): client.sessions.with_raw_response.observe( id="", + stream_response=True, ) @pytest.mark.skip(reason="Prism tests are disabled") @@ -547,7 +837,7 @@ class TestAsyncSessions: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - async def test_method_act(self, async_client: AsyncStagehand) -> None: + async def test_method_act_overload_1(self, async_client: AsyncStagehand) -> None: session = await async_client.sessions.act( id="c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123", input="Click the login button", @@ -556,7 +846,7 @@ async def test_method_act(self, async_client: AsyncStagehand) -> None: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - async def test_method_act_with_all_params(self, async_client: AsyncStagehand) -> None: + async def test_method_act_with_all_params_overload_1(self, async_client: AsyncStagehand) -> None: session = await async_client.sessions.act( id="c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123", input="Click the login button", @@ -566,6 +856,7 @@ async def test_method_act_with_all_params(self, async_client: AsyncStagehand) -> "timeout": 30000, "variables": {"username": "john_doe"}, }, + stream_response=False, x_language="typescript", x_sdk_version="3.0.6", x_sent_at=parse_datetime("2025-01-15T10:30:00Z"), @@ -575,7 +866,7 @@ async def test_method_act_with_all_params(self, async_client: AsyncStagehand) -> @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - async def test_raw_response_act(self, async_client: AsyncStagehand) -> None: + async def test_raw_response_act_overload_1(self, async_client: AsyncStagehand) -> None: response = await async_client.sessions.with_raw_response.act( id="c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123", input="Click the login button", @@ -588,7 +879,7 @@ async def test_raw_response_act(self, async_client: AsyncStagehand) -> None: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - async def test_streaming_response_act(self, async_client: AsyncStagehand) -> None: + async def test_streaming_response_act_overload_1(self, async_client: AsyncStagehand) -> None: async with async_client.sessions.with_streaming_response.act( id="c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123", input="Click the login button", @@ -603,13 +894,82 @@ async def test_streaming_response_act(self, async_client: AsyncStagehand) -> Non @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - async def test_path_params_act(self, async_client: AsyncStagehand) -> None: + async def test_path_params_act_overload_1(self, async_client: AsyncStagehand) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): await async_client.sessions.with_raw_response.act( id="", input="Click the login button", ) + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_method_act_overload_2(self, async_client: AsyncStagehand) -> None: + session_stream = await async_client.sessions.act( + id="c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123", + input="Click the login button", + stream_response=True, + ) + await session_stream.response.aclose() + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_method_act_with_all_params_overload_2(self, async_client: AsyncStagehand) -> None: + session_stream = await async_client.sessions.act( + id="c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123", + input="Click the login button", + stream_response=True, + frame_id="frameId", + options={ + "model": "openai/gpt-5-nano", + "timeout": 30000, + "variables": {"username": "john_doe"}, + }, + x_language="typescript", + x_sdk_version="3.0.6", + x_sent_at=parse_datetime("2025-01-15T10:30:00Z"), + x_stream_response="true", + ) + await session_stream.response.aclose() + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_raw_response_act_overload_2(self, async_client: AsyncStagehand) -> None: + response = await async_client.sessions.with_raw_response.act( + id="c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123", + input="Click the login button", + stream_response=True, + ) + + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + stream = await response.parse() + await stream.close() + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_streaming_response_act_overload_2(self, async_client: AsyncStagehand) -> None: + async with async_client.sessions.with_streaming_response.act( + id="c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123", + input="Click the login button", + stream_response=True, + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + stream = await response.parse() + await stream.close() + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_path_params_act_overload_2(self, async_client: AsyncStagehand) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.sessions.with_raw_response.act( + id="", + input="Click the login button", + stream_response=True, + ) + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_method_end(self, async_client: AsyncStagehand) -> None: @@ -666,7 +1026,7 @@ async def test_path_params_end(self, async_client: AsyncStagehand) -> None: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - async def test_method_execute(self, async_client: AsyncStagehand) -> None: + async def test_method_execute_overload_1(self, async_client: AsyncStagehand) -> None: session = await async_client.sessions.execute( id="c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123", agent_config={}, @@ -678,7 +1038,7 @@ async def test_method_execute(self, async_client: AsyncStagehand) -> None: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - async def test_method_execute_with_all_params(self, async_client: AsyncStagehand) -> None: + async def test_method_execute_with_all_params_overload_1(self, async_client: AsyncStagehand) -> None: session = await async_client.sessions.execute( id="c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123", agent_config={ @@ -692,6 +1052,7 @@ async def test_method_execute_with_all_params(self, async_client: AsyncStagehand "max_steps": 20, }, frame_id="frameId", + stream_response=False, x_language="typescript", x_sdk_version="3.0.6", x_sent_at=parse_datetime("2025-01-15T10:30:00Z"), @@ -701,7 +1062,7 @@ async def test_method_execute_with_all_params(self, async_client: AsyncStagehand @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - async def test_raw_response_execute(self, async_client: AsyncStagehand) -> None: + async def test_raw_response_execute_overload_1(self, async_client: AsyncStagehand) -> None: response = await async_client.sessions.with_raw_response.execute( id="c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123", agent_config={}, @@ -717,7 +1078,7 @@ async def test_raw_response_execute(self, async_client: AsyncStagehand) -> None: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - async def test_streaming_response_execute(self, async_client: AsyncStagehand) -> None: + async def test_streaming_response_execute_overload_1(self, async_client: AsyncStagehand) -> None: async with async_client.sessions.with_streaming_response.execute( id="c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123", agent_config={}, @@ -735,7 +1096,7 @@ async def test_streaming_response_execute(self, async_client: AsyncStagehand) -> @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - async def test_path_params_execute(self, async_client: AsyncStagehand) -> None: + async def test_path_params_execute_overload_1(self, async_client: AsyncStagehand) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): await async_client.sessions.with_raw_response.execute( id="", @@ -747,7 +1108,92 @@ async def test_path_params_execute(self, async_client: AsyncStagehand) -> None: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - async def test_method_extract(self, async_client: AsyncStagehand) -> None: + async def test_method_execute_overload_2(self, async_client: AsyncStagehand) -> None: + session_stream = await async_client.sessions.execute( + id="c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123", + agent_config={}, + execute_options={ + "instruction": "Log in with username 'demo' and password 'test123', then navigate to settings" + }, + stream_response=True, + ) + await session_stream.response.aclose() + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_method_execute_with_all_params_overload_2(self, async_client: AsyncStagehand) -> None: + session_stream = await async_client.sessions.execute( + id="c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123", + agent_config={ + "cua": True, + "model": "openai/gpt-5-nano", + "system_prompt": "systemPrompt", + }, + execute_options={ + "instruction": "Log in with username 'demo' and password 'test123', then navigate to settings", + "highlight_cursor": True, + "max_steps": 20, + }, + stream_response=True, + frame_id="frameId", + x_language="typescript", + x_sdk_version="3.0.6", + x_sent_at=parse_datetime("2025-01-15T10:30:00Z"), + x_stream_response="true", + ) + await session_stream.response.aclose() + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_raw_response_execute_overload_2(self, async_client: AsyncStagehand) -> None: + response = await async_client.sessions.with_raw_response.execute( + id="c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123", + agent_config={}, + execute_options={ + "instruction": "Log in with username 'demo' and password 'test123', then navigate to settings" + }, + stream_response=True, + ) + + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + stream = await response.parse() + await stream.close() + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_streaming_response_execute_overload_2(self, async_client: AsyncStagehand) -> None: + async with async_client.sessions.with_streaming_response.execute( + id="c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123", + agent_config={}, + execute_options={ + "instruction": "Log in with username 'demo' and password 'test123', then navigate to settings" + }, + stream_response=True, + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + stream = await response.parse() + await stream.close() + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_path_params_execute_overload_2(self, async_client: AsyncStagehand) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.sessions.with_raw_response.execute( + id="", + agent_config={}, + execute_options={ + "instruction": "Log in with username 'demo' and password 'test123', then navigate to settings" + }, + stream_response=True, + ) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_method_extract_overload_1(self, async_client: AsyncStagehand) -> None: session = await async_client.sessions.extract( id="c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123", ) @@ -755,7 +1201,7 @@ async def test_method_extract(self, async_client: AsyncStagehand) -> None: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - async def test_method_extract_with_all_params(self, async_client: AsyncStagehand) -> None: + async def test_method_extract_with_all_params_overload_1(self, async_client: AsyncStagehand) -> None: session = await async_client.sessions.extract( id="c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123", frame_id="frameId", @@ -766,6 +1212,7 @@ async def test_method_extract_with_all_params(self, async_client: AsyncStagehand "timeout": 30000, }, schema={"foo": "bar"}, + stream_response=False, x_language="typescript", x_sdk_version="3.0.6", x_sent_at=parse_datetime("2025-01-15T10:30:00Z"), @@ -775,7 +1222,7 @@ async def test_method_extract_with_all_params(self, async_client: AsyncStagehand @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - async def test_raw_response_extract(self, async_client: AsyncStagehand) -> None: + async def test_raw_response_extract_overload_1(self, async_client: AsyncStagehand) -> None: response = await async_client.sessions.with_raw_response.extract( id="c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123", ) @@ -787,7 +1234,7 @@ async def test_raw_response_extract(self, async_client: AsyncStagehand) -> None: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - async def test_streaming_response_extract(self, async_client: AsyncStagehand) -> None: + async def test_streaming_response_extract_overload_1(self, async_client: AsyncStagehand) -> None: async with async_client.sessions.with_streaming_response.extract( id="c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123", ) as response: @@ -801,10 +1248,76 @@ async def test_streaming_response_extract(self, async_client: AsyncStagehand) -> @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - async def test_path_params_extract(self, async_client: AsyncStagehand) -> None: + async def test_path_params_extract_overload_1(self, async_client: AsyncStagehand) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.sessions.with_raw_response.extract( + id="", + ) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_method_extract_overload_2(self, async_client: AsyncStagehand) -> None: + session_stream = await async_client.sessions.extract( + id="c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123", + stream_response=True, + ) + await session_stream.response.aclose() + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_method_extract_with_all_params_overload_2(self, async_client: AsyncStagehand) -> None: + session_stream = await async_client.sessions.extract( + id="c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123", + stream_response=True, + frame_id="frameId", + instruction="Extract all product names and prices from the page", + options={ + "model": "openai/gpt-5-nano", + "selector": "#main-content", + "timeout": 30000, + }, + schema={"foo": "bar"}, + x_language="typescript", + x_sdk_version="3.0.6", + x_sent_at=parse_datetime("2025-01-15T10:30:00Z"), + x_stream_response="true", + ) + await session_stream.response.aclose() + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_raw_response_extract_overload_2(self, async_client: AsyncStagehand) -> None: + response = await async_client.sessions.with_raw_response.extract( + id="c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123", + stream_response=True, + ) + + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + stream = await response.parse() + await stream.close() + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_streaming_response_extract_overload_2(self, async_client: AsyncStagehand) -> None: + async with async_client.sessions.with_streaming_response.extract( + id="c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123", + stream_response=True, + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + stream = await response.parse() + await stream.close() + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_path_params_extract_overload_2(self, async_client: AsyncStagehand) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): await async_client.sessions.with_raw_response.extract( id="", + stream_response=True, ) @pytest.mark.skip(reason="Prism tests are disabled") @@ -828,6 +1341,7 @@ async def test_method_navigate_with_all_params(self, async_client: AsyncStagehan "timeout": 30000, "wait_until": "networkidle", }, + stream_response=True, x_language="typescript", x_sdk_version="3.0.6", x_sent_at=parse_datetime("2025-01-15T10:30:00Z"), @@ -874,7 +1388,7 @@ async def test_path_params_navigate(self, async_client: AsyncStagehand) -> None: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - async def test_method_observe(self, async_client: AsyncStagehand) -> None: + async def test_method_observe_overload_1(self, async_client: AsyncStagehand) -> None: session = await async_client.sessions.observe( id="c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123", ) @@ -882,7 +1396,7 @@ async def test_method_observe(self, async_client: AsyncStagehand) -> None: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - async def test_method_observe_with_all_params(self, async_client: AsyncStagehand) -> None: + async def test_method_observe_with_all_params_overload_1(self, async_client: AsyncStagehand) -> None: session = await async_client.sessions.observe( id="c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123", frame_id="frameId", @@ -892,6 +1406,7 @@ async def test_method_observe_with_all_params(self, async_client: AsyncStagehand "selector": "nav", "timeout": 30000, }, + stream_response=False, x_language="typescript", x_sdk_version="3.0.6", x_sent_at=parse_datetime("2025-01-15T10:30:00Z"), @@ -901,7 +1416,7 @@ async def test_method_observe_with_all_params(self, async_client: AsyncStagehand @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - async def test_raw_response_observe(self, async_client: AsyncStagehand) -> None: + async def test_raw_response_observe_overload_1(self, async_client: AsyncStagehand) -> None: response = await async_client.sessions.with_raw_response.observe( id="c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123", ) @@ -913,7 +1428,7 @@ async def test_raw_response_observe(self, async_client: AsyncStagehand) -> None: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - async def test_streaming_response_observe(self, async_client: AsyncStagehand) -> None: + async def test_streaming_response_observe_overload_1(self, async_client: AsyncStagehand) -> None: async with async_client.sessions.with_streaming_response.observe( id="c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123", ) as response: @@ -927,10 +1442,75 @@ async def test_streaming_response_observe(self, async_client: AsyncStagehand) -> @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - async def test_path_params_observe(self, async_client: AsyncStagehand) -> None: + async def test_path_params_observe_overload_1(self, async_client: AsyncStagehand) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.sessions.with_raw_response.observe( + id="", + ) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_method_observe_overload_2(self, async_client: AsyncStagehand) -> None: + session_stream = await async_client.sessions.observe( + id="c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123", + stream_response=True, + ) + await session_stream.response.aclose() + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_method_observe_with_all_params_overload_2(self, async_client: AsyncStagehand) -> None: + session_stream = await async_client.sessions.observe( + id="c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123", + stream_response=True, + frame_id="frameId", + instruction="Find all clickable navigation links", + options={ + "model": "openai/gpt-5-nano", + "selector": "nav", + "timeout": 30000, + }, + x_language="typescript", + x_sdk_version="3.0.6", + x_sent_at=parse_datetime("2025-01-15T10:30:00Z"), + x_stream_response="true", + ) + await session_stream.response.aclose() + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_raw_response_observe_overload_2(self, async_client: AsyncStagehand) -> None: + response = await async_client.sessions.with_raw_response.observe( + id="c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123", + stream_response=True, + ) + + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + stream = await response.parse() + await stream.close() + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_streaming_response_observe_overload_2(self, async_client: AsyncStagehand) -> None: + async with async_client.sessions.with_streaming_response.observe( + id="c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123", + stream_response=True, + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + stream = await response.parse() + await stream.close() + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_path_params_observe_overload_2(self, async_client: AsyncStagehand) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): await async_client.sessions.with_raw_response.observe( id="", + stream_response=True, ) @pytest.mark.skip(reason="Prism tests are disabled") diff --git a/tests/test_client.py b/tests/test_client.py index 8918f89f..b1d79c7a 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -22,6 +22,7 @@ from stagehand._types import Omit from stagehand._utils import asyncify from stagehand._models import BaseModel, FinalRequestOptions +from stagehand._streaming import Stream, AsyncStream from stagehand._exceptions import APIStatusError, StagehandError, APITimeoutError, APIResponseValidationError from stagehand._base_client import ( DEFAULT_TIMEOUT, @@ -811,6 +812,17 @@ def test_client_max_retries_validation(self) -> None: max_retries=cast(Any, None), ) + @pytest.mark.respx(base_url=base_url) + def test_default_stream_cls(self, respx_mock: MockRouter, client: Stagehand) -> None: + class Model(BaseModel): + name: str + + respx_mock.post("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"})) + + stream = client.post("/foo", cast_to=Model, stream=True, stream_cls=Stream[Model]) + assert isinstance(stream, Stream) + stream.response.close() + @pytest.mark.respx(base_url=base_url) def test_received_text_for_expected_json(self, respx_mock: MockRouter) -> None: class Model(BaseModel): @@ -1777,6 +1789,17 @@ async def test_client_max_retries_validation(self) -> None: max_retries=cast(Any, None), ) + @pytest.mark.respx(base_url=base_url) + async def test_default_stream_cls(self, respx_mock: MockRouter, async_client: AsyncStagehand) -> None: + class Model(BaseModel): + name: str + + respx_mock.post("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"})) + + stream = await async_client.post("/foo", cast_to=Model, stream=True, stream_cls=AsyncStream[Model]) + assert isinstance(stream, AsyncStream) + await stream.response.aclose() + @pytest.mark.respx(base_url=base_url) async def test_received_text_for_expected_json(self, respx_mock: MockRouter) -> None: class Model(BaseModel): From 53d08675af10f03063ec584404d7ad8346c2d075 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 18 Dec 2025 06:00:36 +0000 Subject: [PATCH 41/88] fix: use async_to_httpx_files in patch method --- src/stagehand/_base_client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/stagehand/_base_client.py b/src/stagehand/_base_client.py index 59a19830..921ca2f7 100644 --- a/src/stagehand/_base_client.py +++ b/src/stagehand/_base_client.py @@ -1774,7 +1774,7 @@ async def patch( options: RequestOptions = {}, ) -> ResponseT: opts = FinalRequestOptions.construct( - method="patch", url=path, json_data=body, files=to_httpx_files(files), **options + method="patch", url=path, json_data=body, files=await async_to_httpx_files(files), **options ) return await self.request(cast_to, opts) From 78e80fac50b7d4a37a83a1e07f2aa8a4406289e2 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 19 Dec 2025 05:39:46 +0000 Subject: [PATCH 42/88] chore(internal): add `--fix` argument to lint script --- scripts/lint | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/scripts/lint b/scripts/lint index 976e1608..f692ed66 100755 --- a/scripts/lint +++ b/scripts/lint @@ -4,8 +4,13 @@ set -e cd "$(dirname "$0")/.." -echo "==> Running ruff" -uv run ruff check . +if [ "$1" = "--fix" ]; then + echo "==> Running ruff with --fix" + uv run ruff check . --fix +else + echo "==> Running ruff" + uv run ruff check . +fi echo "==> Running pyright" uv run pyright From 396da87deff5a4b7b0ebd194f3490a046403285b Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 19 Dec 2025 23:35:58 +0000 Subject: [PATCH 43/88] feat(api): manual updates --- .stats.yml | 4 ++-- src/stagehand/resources/sessions.py | 16 ++++++------- src/stagehand/types/__init__.py | 1 - src/stagehand/types/action.py | 23 ------------------ src/stagehand/types/action_param.py | 6 ++++- src/stagehand/types/model_config_param.py | 5 +++- src/stagehand/types/session_act_response.py | 24 ++++++++++++++++--- src/stagehand/types/session_execute_params.py | 3 +++ .../types/session_observe_response.py | 24 ++++++++++++++++--- src/stagehand/types/session_start_params.py | 7 +++--- src/stagehand/types/session_start_response.py | 11 ++++++--- src/stagehand/types/stream_event.py | 2 +- tests/api_resources/test_sessions.py | 14 ++++++----- 13 files changed, 84 insertions(+), 56 deletions(-) delete mode 100644 src/stagehand/types/action.py diff --git a/.stats.yml b/.stats.yml index 5dba58c6..d90df611 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 7 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fstagehand-f7d6b6489159f611a2bfdc267ce0a6fc0455bed1ffa0c310044baaa5d8381b9b.yml -openapi_spec_hash: cd88d8068abfde8382da0bed674e440c +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fstagehand-4fb17cafc413ae3d575e3268602b01d2d0e9ebeb734a41b6086b3353ff0d2523.yml +openapi_spec_hash: 8d48d8564849246f6f14d900c6c5f60c config_hash: 5c69fb596588b8ace08203858518c149 diff --git a/src/stagehand/resources/sessions.py b/src/stagehand/resources/sessions.py index 7b317669..1ad78f32 100644 --- a/src/stagehand/resources/sessions.py +++ b/src/stagehand/resources/sessions.py @@ -1028,12 +1028,11 @@ def start( browser: session_start_params.Browser | Omit = omit, browserbase_session_create_params: session_start_params.BrowserbaseSessionCreateParams | Omit = omit, browserbase_session_id: str | Omit = omit, - debug_dom: bool | Omit = omit, dom_settle_timeout_ms: float | Omit = omit, experimental: bool | Omit = omit, self_heal: bool | Omit = omit, system_prompt: str | Omit = omit, - verbose: int | Omit = omit, + verbose: Literal["0", "1", "2"] | Omit = omit, wait_for_captcha_solves: bool | Omit = omit, x_language: Literal["typescript", "python", "playground"] | Omit = omit, x_sdk_version: str | Omit = omit, @@ -1054,7 +1053,7 @@ def start( Args: model_name: Model name to use for AI operations - act_timeout_ms: Timeout in ms for act operations + act_timeout_ms: Timeout in ms for act operations (deprecated, v2 only) browserbase_session_id: Existing Browserbase session ID to resume @@ -1066,6 +1065,8 @@ def start( verbose: Logging verbosity level (0=quiet, 1=normal, 2=debug) + wait_for_captcha_solves: Wait for captcha solves (deprecated, v2 only) + x_language: Client SDK language x_sdk_version: Version of the Stagehand SDK @@ -1102,7 +1103,6 @@ def start( "browser": browser, "browserbase_session_create_params": browserbase_session_create_params, "browserbase_session_id": browserbase_session_id, - "debug_dom": debug_dom, "dom_settle_timeout_ms": dom_settle_timeout_ms, "experimental": experimental, "self_heal": self_heal, @@ -2107,12 +2107,11 @@ async def start( browser: session_start_params.Browser | Omit = omit, browserbase_session_create_params: session_start_params.BrowserbaseSessionCreateParams | Omit = omit, browserbase_session_id: str | Omit = omit, - debug_dom: bool | Omit = omit, dom_settle_timeout_ms: float | Omit = omit, experimental: bool | Omit = omit, self_heal: bool | Omit = omit, system_prompt: str | Omit = omit, - verbose: int | Omit = omit, + verbose: Literal["0", "1", "2"] | Omit = omit, wait_for_captcha_solves: bool | Omit = omit, x_language: Literal["typescript", "python", "playground"] | Omit = omit, x_sdk_version: str | Omit = omit, @@ -2133,7 +2132,7 @@ async def start( Args: model_name: Model name to use for AI operations - act_timeout_ms: Timeout in ms for act operations + act_timeout_ms: Timeout in ms for act operations (deprecated, v2 only) browserbase_session_id: Existing Browserbase session ID to resume @@ -2145,6 +2144,8 @@ async def start( verbose: Logging verbosity level (0=quiet, 1=normal, 2=debug) + wait_for_captcha_solves: Wait for captcha solves (deprecated, v2 only) + x_language: Client SDK language x_sdk_version: Version of the Stagehand SDK @@ -2181,7 +2182,6 @@ async def start( "browser": browser, "browserbase_session_create_params": browserbase_session_create_params, "browserbase_session_id": browserbase_session_id, - "debug_dom": debug_dom, "dom_settle_timeout_ms": dom_settle_timeout_ms, "experimental": experimental, "self_heal": self_heal, diff --git a/src/stagehand/types/__init__.py b/src/stagehand/types/__init__.py index 890eccb0..46ce9d80 100644 --- a/src/stagehand/types/__init__.py +++ b/src/stagehand/types/__init__.py @@ -2,7 +2,6 @@ from __future__ import annotations -from .action import Action as Action from .action_param import ActionParam as ActionParam from .stream_event import StreamEvent as StreamEvent from .model_config_param import ModelConfigParam as ModelConfigParam diff --git a/src/stagehand/types/action.py b/src/stagehand/types/action.py deleted file mode 100644 index 02ca3ebc..00000000 --- a/src/stagehand/types/action.py +++ /dev/null @@ -1,23 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from typing import List, Optional - -from .._models import BaseModel - -__all__ = ["Action"] - - -class Action(BaseModel): - """Action object returned by observe and used by act""" - - description: str - """Human-readable description of the action""" - - selector: str - """CSS selector or XPath for the element""" - - arguments: Optional[List[str]] = None - """Arguments to pass to the method""" - - method: Optional[str] = None - """The method to execute (click, fill, etc.)""" diff --git a/src/stagehand/types/action_param.py b/src/stagehand/types/action_param.py index 6ef78c2d..4706fb2b 100644 --- a/src/stagehand/types/action_param.py +++ b/src/stagehand/types/action_param.py @@ -2,9 +2,10 @@ from __future__ import annotations -from typing_extensions import Required, TypedDict +from typing_extensions import Required, Annotated, TypedDict from .._types import SequenceNotStr +from .._utils import PropertyInfo __all__ = ["ActionParam"] @@ -21,5 +22,8 @@ class ActionParam(TypedDict, total=False): arguments: SequenceNotStr[str] """Arguments to pass to the method""" + backend_node_id: Annotated[float, PropertyInfo(alias="backendNodeId")] + """Backend node ID for the element""" + method: str """The method to execute (click, fill, etc.)""" diff --git a/src/stagehand/types/model_config_param.py b/src/stagehand/types/model_config_param.py index b6026e77..8d3df0ef 100644 --- a/src/stagehand/types/model_config_param.py +++ b/src/stagehand/types/model_config_param.py @@ -3,7 +3,7 @@ from __future__ import annotations from typing import Union -from typing_extensions import Required, Annotated, TypeAlias, TypedDict +from typing_extensions import Literal, Required, Annotated, TypeAlias, TypedDict from .._utils import PropertyInfo @@ -20,5 +20,8 @@ class ModelConfigObject(TypedDict, total=False): base_url: Annotated[str, PropertyInfo(alias="baseURL")] """Base URL for the model provider""" + provider: Literal["openai", "anthropic", "google", "microsoft"] + """AI provider for the model (or provide a baseURL endpoint instead)""" + ModelConfigParam: TypeAlias = Union[str, ModelConfigObject] diff --git a/src/stagehand/types/session_act_response.py b/src/stagehand/types/session_act_response.py index 94649e38..888ce8b7 100644 --- a/src/stagehand/types/session_act_response.py +++ b/src/stagehand/types/session_act_response.py @@ -4,17 +4,35 @@ from pydantic import Field as FieldInfo -from .action import Action from .._models import BaseModel -__all__ = ["SessionActResponse", "Data", "DataResult"] +__all__ = ["SessionActResponse", "Data", "DataResult", "DataResultAction"] + + +class DataResultAction(BaseModel): + """Action object returned by observe and used by act""" + + description: str + """Human-readable description of the action""" + + selector: str + """CSS selector or XPath for the element""" + + arguments: Optional[List[str]] = None + """Arguments to pass to the method""" + + backend_node_id: Optional[float] = FieldInfo(alias="backendNodeId", default=None) + """Backend node ID for the element""" + + method: Optional[str] = None + """The method to execute (click, fill, etc.)""" class DataResult(BaseModel): action_description: str = FieldInfo(alias="actionDescription") """Description of the action that was performed""" - actions: List[Action] + actions: List[DataResultAction] """List of actions that were executed""" message: str diff --git a/src/stagehand/types/session_execute_params.py b/src/stagehand/types/session_execute_params.py index 2957d8e8..794181d7 100644 --- a/src/stagehand/types/session_execute_params.py +++ b/src/stagehand/types/session_execute_params.py @@ -49,6 +49,9 @@ class AgentConfig(TypedDict, total=False): 'anthropic/claude-4.5-opus') """ + provider: Literal["openai", "anthropic", "google", "microsoft"] + """AI provider for the agent (legacy, use model: openai/gpt-5-nano instead)""" + system_prompt: Annotated[str, PropertyInfo(alias="systemPrompt")] """Custom system prompt for the agent""" diff --git a/src/stagehand/types/session_observe_response.py b/src/stagehand/types/session_observe_response.py index 6df90a96..e683a839 100644 --- a/src/stagehand/types/session_observe_response.py +++ b/src/stagehand/types/session_observe_response.py @@ -4,14 +4,32 @@ from pydantic import Field as FieldInfo -from .action import Action from .._models import BaseModel -__all__ = ["SessionObserveResponse", "Data"] +__all__ = ["SessionObserveResponse", "Data", "DataResult"] + + +class DataResult(BaseModel): + """Action object returned by observe and used by act""" + + description: str + """Human-readable description of the action""" + + selector: str + """CSS selector or XPath for the element""" + + arguments: Optional[List[str]] = None + """Arguments to pass to the method""" + + backend_node_id: Optional[float] = FieldInfo(alias="backendNodeId", default=None) + """Backend node ID for the element""" + + method: Optional[str] = None + """The method to execute (click, fill, etc.)""" class Data(BaseModel): - result: List[Action] + result: List[DataResult] action_id: Optional[str] = FieldInfo(alias="actionId", default=None) """Action ID for tracking""" diff --git a/src/stagehand/types/session_start_params.py b/src/stagehand/types/session_start_params.py index 50f0538e..50acb7ce 100644 --- a/src/stagehand/types/session_start_params.py +++ b/src/stagehand/types/session_start_params.py @@ -33,7 +33,7 @@ class SessionStartParams(TypedDict, total=False): """Model name to use for AI operations""" act_timeout_ms: Annotated[float, PropertyInfo(alias="actTimeoutMs")] - """Timeout in ms for act operations""" + """Timeout in ms for act operations (deprecated, v2 only)""" browser: Browser @@ -44,8 +44,6 @@ class SessionStartParams(TypedDict, total=False): browserbase_session_id: Annotated[str, PropertyInfo(alias="browserbaseSessionID")] """Existing Browserbase session ID to resume""" - debug_dom: Annotated[bool, PropertyInfo(alias="debugDom")] - dom_settle_timeout_ms: Annotated[float, PropertyInfo(alias="domSettleTimeoutMs")] """Timeout in ms to wait for DOM to settle""" @@ -57,10 +55,11 @@ class SessionStartParams(TypedDict, total=False): system_prompt: Annotated[str, PropertyInfo(alias="systemPrompt")] """Custom system prompt for AI operations""" - verbose: int + verbose: Literal["0", "1", "2"] """Logging verbosity level (0=quiet, 1=normal, 2=debug)""" wait_for_captcha_solves: Annotated[bool, PropertyInfo(alias="waitForCaptchaSolves")] + """Wait for captcha solves (deprecated, v2 only)""" x_language: Annotated[Literal["typescript", "python", "playground"], PropertyInfo(alias="x-language")] """Client SDK language""" diff --git a/src/stagehand/types/session_start_response.py b/src/stagehand/types/session_start_response.py index 1bec1267..1f9a5d7e 100644 --- a/src/stagehand/types/session_start_response.py +++ b/src/stagehand/types/session_start_response.py @@ -1,5 +1,7 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. +from typing import Optional + from pydantic import Field as FieldInfo from .._models import BaseModel @@ -10,12 +12,15 @@ class Data(BaseModel): available: bool - connect_url: str = FieldInfo(alias="connectUrl") - """CDP WebSocket URL for connecting to the Browserbase cloud browser""" - session_id: str = FieldInfo(alias="sessionId") """Unique Browserbase session identifier""" + cdp_url: Optional[str] = FieldInfo(alias="cdpUrl", default=None) + """ + CDP WebSocket URL for connecting to the Browserbase cloud browser (present when + available) + """ + class SessionStartResponse(BaseModel): data: Data diff --git a/src/stagehand/types/stream_event.py b/src/stagehand/types/stream_event.py index 2504a1d1..3d5b21ee 100644 --- a/src/stagehand/types/stream_event.py +++ b/src/stagehand/types/stream_event.py @@ -32,7 +32,7 @@ class DataStreamEventLogDataOutput(BaseModel): class StreamEvent(BaseModel): """Server-Sent Event emitted during streaming responses. - Events are sent as `data: \n\n`. + Events are sent as `data: \n\n`. Key order: data (with status first), type, id. """ id: str diff --git a/tests/api_resources/test_sessions.py b/tests/api_resources/test_sessions.py index 84503a8a..308fa5f5 100644 --- a/tests/api_resources/test_sessions.py +++ b/tests/api_resources/test_sessions.py @@ -235,6 +235,7 @@ def test_method_execute_with_all_params_overload_1(self, client: Stagehand) -> N agent_config={ "cua": True, "model": "openai/gpt-5-nano", + "provider": "openai", "system_prompt": "systemPrompt", }, execute_options={ @@ -318,6 +319,7 @@ def test_method_execute_with_all_params_overload_2(self, client: Stagehand) -> N agent_config={ "cua": True, "model": "openai/gpt-5-nano", + "provider": "openai", "system_prompt": "systemPrompt", }, execute_options={ @@ -717,7 +719,7 @@ def test_method_start(self, client: Stagehand) -> None: def test_method_start_with_all_params(self, client: Stagehand) -> None: session = client.sessions.start( model_name="gpt-4o", - act_timeout_ms=30000, + act_timeout_ms=0, browser={ "cdp_url": "ws://localhost:9222", "launch_options": { @@ -789,12 +791,11 @@ def test_method_start_with_all_params(self, client: Stagehand) -> None: "user_metadata": {"foo": "bar"}, }, browserbase_session_id="browserbaseSessionID", - debug_dom=True, dom_settle_timeout_ms=5000, experimental=True, self_heal=True, system_prompt="systemPrompt", - verbose=1, + verbose="1", wait_for_captcha_solves=True, x_language="typescript", x_sdk_version="3.0.6", @@ -1044,6 +1045,7 @@ async def test_method_execute_with_all_params_overload_1(self, async_client: Asy agent_config={ "cua": True, "model": "openai/gpt-5-nano", + "provider": "openai", "system_prompt": "systemPrompt", }, execute_options={ @@ -1127,6 +1129,7 @@ async def test_method_execute_with_all_params_overload_2(self, async_client: Asy agent_config={ "cua": True, "model": "openai/gpt-5-nano", + "provider": "openai", "system_prompt": "systemPrompt", }, execute_options={ @@ -1526,7 +1529,7 @@ async def test_method_start(self, async_client: AsyncStagehand) -> None: async def test_method_start_with_all_params(self, async_client: AsyncStagehand) -> None: session = await async_client.sessions.start( model_name="gpt-4o", - act_timeout_ms=30000, + act_timeout_ms=0, browser={ "cdp_url": "ws://localhost:9222", "launch_options": { @@ -1598,12 +1601,11 @@ async def test_method_start_with_all_params(self, async_client: AsyncStagehand) "user_metadata": {"foo": "bar"}, }, browserbase_session_id="browserbaseSessionID", - debug_dom=True, dom_settle_timeout_ms=5000, experimental=True, self_heal=True, system_prompt="systemPrompt", - verbose=1, + verbose="1", wait_for_captcha_solves=True, x_language="typescript", x_sdk_version="3.0.6", From 36a1645ad70af68ef592d03cc4bf0c8d6a975fcb Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 20 Dec 2025 04:57:21 +0000 Subject: [PATCH 44/88] docs: add more examples --- README.md | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/README.md b/README.md index 683f515e..a92fa2c7 100644 --- a/README.md +++ b/README.md @@ -127,6 +127,40 @@ async def main() -> None: asyncio.run(main()) ``` +## Streaming responses + +We provide support for streaming responses using Server Side Events (SSE). + +```python +from stagehand import Stagehand + +client = Stagehand() + +stream = client.sessions.act( + id="00000000-your-session-id-000000000000", + input="click the first link on the page", + stream_response=True, +) +for response in stream: + print(response.data) +``` + +The async client uses the exact same interface. + +```python +from stagehand import AsyncStagehand + +client = AsyncStagehand() + +stream = await client.sessions.act( + id="00000000-your-session-id-000000000000", + input="click the first link on the page", + stream_response=True, +) +async for response in stream: + print(response.data) +``` + ## Using types Nested request parameters are [TypedDicts](https://docs.python.org/3/library/typing.html#typing.TypedDict). Responses are [Pydantic models](https://docs.pydantic.dev) which also provide helper methods for things like: From ee6d4357d0272b315e5164a85d8b9e532d5c05a8 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 22 Dec 2025 21:50:09 +0000 Subject: [PATCH 45/88] feat: [STG-1053] [server] Use fastify-zod-openapi + zod v4 for openapi generation --- .stats.yml | 4 ++-- src/stagehand/resources/sessions.py | 4 ++-- src/stagehand/types/session_start_params.py | 2 +- tests/api_resources/test_sessions.py | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.stats.yml b/.stats.yml index d90df611..2c7a5238 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 7 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fstagehand-4fb17cafc413ae3d575e3268602b01d2d0e9ebeb734a41b6086b3353ff0d2523.yml -openapi_spec_hash: 8d48d8564849246f6f14d900c6c5f60c +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fstagehand-ed52466945f2f8dfd3814a29e948d7bf30af7b76a7a7689079c03b8baf64e26f.yml +openapi_spec_hash: 5d57aaf2362b0d882372dbf76477ba23 config_hash: 5c69fb596588b8ace08203858518c149 diff --git a/src/stagehand/resources/sessions.py b/src/stagehand/resources/sessions.py index 1ad78f32..19fe9256 100644 --- a/src/stagehand/resources/sessions.py +++ b/src/stagehand/resources/sessions.py @@ -1032,7 +1032,7 @@ def start( experimental: bool | Omit = omit, self_heal: bool | Omit = omit, system_prompt: str | Omit = omit, - verbose: Literal["0", "1", "2"] | Omit = omit, + verbose: Literal[0, 1, 2] | Omit = omit, wait_for_captcha_solves: bool | Omit = omit, x_language: Literal["typescript", "python", "playground"] | Omit = omit, x_sdk_version: str | Omit = omit, @@ -2111,7 +2111,7 @@ async def start( experimental: bool | Omit = omit, self_heal: bool | Omit = omit, system_prompt: str | Omit = omit, - verbose: Literal["0", "1", "2"] | Omit = omit, + verbose: Literal[0, 1, 2] | Omit = omit, wait_for_captcha_solves: bool | Omit = omit, x_language: Literal["typescript", "python", "playground"] | Omit = omit, x_sdk_version: str | Omit = omit, diff --git a/src/stagehand/types/session_start_params.py b/src/stagehand/types/session_start_params.py index 50acb7ce..479d1fef 100644 --- a/src/stagehand/types/session_start_params.py +++ b/src/stagehand/types/session_start_params.py @@ -55,7 +55,7 @@ class SessionStartParams(TypedDict, total=False): system_prompt: Annotated[str, PropertyInfo(alias="systemPrompt")] """Custom system prompt for AI operations""" - verbose: Literal["0", "1", "2"] + verbose: Literal[0, 1, 2] """Logging verbosity level (0=quiet, 1=normal, 2=debug)""" wait_for_captcha_solves: Annotated[bool, PropertyInfo(alias="waitForCaptchaSolves")] diff --git a/tests/api_resources/test_sessions.py b/tests/api_resources/test_sessions.py index 308fa5f5..daef296d 100644 --- a/tests/api_resources/test_sessions.py +++ b/tests/api_resources/test_sessions.py @@ -795,7 +795,7 @@ def test_method_start_with_all_params(self, client: Stagehand) -> None: experimental=True, self_heal=True, system_prompt="systemPrompt", - verbose="1", + verbose=1, wait_for_captcha_solves=True, x_language="typescript", x_sdk_version="3.0.6", @@ -1605,7 +1605,7 @@ async def test_method_start_with_all_params(self, async_client: AsyncStagehand) experimental=True, self_heal=True, system_prompt="systemPrompt", - verbose="1", + verbose=1, wait_for_captcha_solves=True, x_language="typescript", x_sdk_version="3.0.6", From 507e6b2e32e653d83b550e1e4020682bf306bf8a Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 23 Dec 2025 19:53:48 +0000 Subject: [PATCH 46/88] chore: update SDK settings --- .stats.yml | 2 +- README.md | 11 ++++------- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/.stats.yml b/.stats.yml index 2c7a5238..21604fbf 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 7 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fstagehand-ed52466945f2f8dfd3814a29e948d7bf30af7b76a7a7689079c03b8baf64e26f.yml openapi_spec_hash: 5d57aaf2362b0d882372dbf76477ba23 -config_hash: 5c69fb596588b8ace08203858518c149 +config_hash: 6b5d2b7e13aea77b1fd038e5eb8c0662 diff --git a/README.md b/README.md index a92fa2c7..3cfa2d0a 100644 --- a/README.md +++ b/README.md @@ -16,13 +16,10 @@ The REST API documentation can be found on [docs.stagehand.dev](https://docs.sta ## Installation ```sh -# install from the production repo -pip install git+ssh://git@github.com/browserbase/stagehand-python#stainless.git +# install from PyPI +pip install stagehand ``` -> [!NOTE] -> Once this package is [published to PyPI](https://www.stainless.com/docs/guides/publish), this will become: `pip install stagehand` - ## Usage The full API of this library can be found in [api.md](api.md). @@ -93,8 +90,8 @@ By default, the async client uses `httpx` for HTTP requests. However, for improv You can enable this by installing `aiohttp`: ```sh -# install from the production repo -pip install 'stagehand[aiohttp] @ git+ssh://git@github.com/browserbase/stagehand-python#stainless.git' +# install from PyPI +pip install stagehand[aiohttp] ``` Then you can enable it by instantiating the client with `http_client=DefaultAioHttpClient()`: From d3b484ec0990cc0477be36cb71a2745c5e089966 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 23 Dec 2025 20:07:23 +0000 Subject: [PATCH 47/88] chore: update SDK settings --- .stats.yml | 2 +- README.md | 6 +- pyproject.toml | 2 +- requirements-dev.lock | 12 +-- uv.lock | 170 +++++++++++++++++++++--------------------- 5 files changed, 96 insertions(+), 96 deletions(-) diff --git a/.stats.yml b/.stats.yml index 21604fbf..5a89fb75 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 7 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fstagehand-ed52466945f2f8dfd3814a29e948d7bf30af7b76a7a7689079c03b8baf64e26f.yml openapi_spec_hash: 5d57aaf2362b0d882372dbf76477ba23 -config_hash: 6b5d2b7e13aea77b1fd038e5eb8c0662 +config_hash: 989ddfee371586e9156b4d484ec0a6cc diff --git a/README.md b/README.md index 3cfa2d0a..6527e46a 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # Stagehand Python API library -[![PyPI version](https://img.shields.io/pypi/v/stagehand.svg?label=pypi%20(stable))](https://pypi.org/project/stagehand/) +[![PyPI version](https://img.shields.io/pypi/v/stagehand-alpha.svg?label=pypi%20(stable))](https://pypi.org/project/stagehand-alpha/) The Stagehand Python library provides convenient access to the Stagehand REST API from any Python 3.9+ application. The library includes type definitions for all request params and response fields, @@ -17,7 +17,7 @@ The REST API documentation can be found on [docs.stagehand.dev](https://docs.sta ```sh # install from PyPI -pip install stagehand +pip install stagehand-alpha ``` ## Usage @@ -91,7 +91,7 @@ You can enable this by installing `aiohttp`: ```sh # install from PyPI -pip install stagehand[aiohttp] +pip install stagehand-alpha[aiohttp] ``` Then you can enable it by instantiating the client with `http_client=DefaultAioHttpClient()`: diff --git a/pyproject.toml b/pyproject.toml index a5160a3b..4e081fd3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [project] -name = "stagehand" +name = "stagehand-alpha" version = "0.0.1" description = "The official Python library for the stagehand API" dynamic = ["readme"] diff --git a/requirements-dev.lock b/requirements-dev.lock index db709b17..0bb435b6 100644 --- a/requirements-dev.lock +++ b/requirements-dev.lock @@ -6,7 +6,7 @@ annotated-types==0.7.0 anyio==4.12.0 # via # httpx - # stagehand + # stagehand-alpha backports-asyncio-runner==1.2.0 ; python_full_version < '3.11' # via pytest-asyncio certifi==2025.11.12 @@ -17,7 +17,7 @@ colorama==0.4.6 ; sys_platform == 'win32' # via pytest dirty-equals==0.11 distro==1.9.0 - # via stagehand + # via stagehand-alpha exceptiongroup==1.3.1 ; python_full_version < '3.11' # via # anyio @@ -31,7 +31,7 @@ httpcore==1.0.9 httpx==0.28.1 # via # respx - # stagehand + # stagehand-alpha idna==3.11 # via # anyio @@ -59,7 +59,7 @@ pathspec==0.12.1 pluggy==1.6.0 # via pytest pydantic==2.12.5 - # via stagehand + # via stagehand-alpha pydantic-core==2.41.5 # via pydantic pygments==2.19.2 @@ -86,7 +86,7 @@ ruff==0.14.7 six==1.17.0 ; python_full_version < '3.10' # via python-dateutil sniffio==1.3.1 - # via stagehand + # via stagehand-alpha time-machine==2.19.0 ; python_full_version < '3.10' time-machine==3.1.0 ; python_full_version >= '3.10' tomli==2.3.0 ; python_full_version < '3.11' @@ -102,7 +102,7 @@ typing-extensions==4.15.0 # pydantic-core # pyright # pytest-asyncio - # stagehand + # stagehand-alpha # typing-inspection typing-inspection==0.4.2 # via pydantic diff --git a/uv.lock b/uv.lock index 69a70754..4e5706cb 100644 --- a/uv.lock +++ b/uv.lock @@ -2,17 +2,17 @@ version = 1 revision = 3 requires-python = ">=3.9" resolution-markers = [ - "python_full_version >= '3.14' and extra != 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2'", - "python_full_version >= '3.10' and python_full_version < '3.14' and extra != 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2'", - "python_full_version < '3.10' and extra != 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2'", - "python_full_version >= '3.10' and extra == 'group-9-stagehand-pydantic-v1' and extra != 'group-9-stagehand-pydantic-v2'", - "python_full_version < '3.10' and extra == 'group-9-stagehand-pydantic-v1' and extra != 'group-9-stagehand-pydantic-v2'", - "python_full_version >= '3.10' and extra != 'group-9-stagehand-pydantic-v1' and extra != 'group-9-stagehand-pydantic-v2'", - "python_full_version < '3.10' and extra != 'group-9-stagehand-pydantic-v1' and extra != 'group-9-stagehand-pydantic-v2'", + "python_full_version >= '3.14' and extra != 'group-15-stagehand-alpha-pydantic-v1' and extra == 'group-15-stagehand-alpha-pydantic-v2'", + "python_full_version >= '3.10' and python_full_version < '3.14' and extra != 'group-15-stagehand-alpha-pydantic-v1' and extra == 'group-15-stagehand-alpha-pydantic-v2'", + "python_full_version < '3.10' and extra != 'group-15-stagehand-alpha-pydantic-v1' and extra == 'group-15-stagehand-alpha-pydantic-v2'", + "python_full_version >= '3.10' and extra == 'group-15-stagehand-alpha-pydantic-v1' and extra != 'group-15-stagehand-alpha-pydantic-v2'", + "python_full_version < '3.10' and extra == 'group-15-stagehand-alpha-pydantic-v1' and extra != 'group-15-stagehand-alpha-pydantic-v2'", + "python_full_version >= '3.10' and extra != 'group-15-stagehand-alpha-pydantic-v1' and extra != 'group-15-stagehand-alpha-pydantic-v2'", + "python_full_version < '3.10' and extra != 'group-15-stagehand-alpha-pydantic-v1' and extra != 'group-15-stagehand-alpha-pydantic-v2'", ] conflicts = [[ - { package = "stagehand", group = "pydantic-v1" }, - { package = "stagehand", group = "pydantic-v2" }, + { package = "stagehand-alpha", group = "pydantic-v1" }, + { package = "stagehand-alpha", group = "pydantic-v2" }, ]] [[package]] @@ -31,7 +31,7 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiohappyeyeballs" }, { name = "aiosignal" }, - { name = "async-timeout", marker = "python_full_version < '3.11' or (extra == 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2')" }, + { name = "async-timeout", marker = "python_full_version < '3.11' or (extra == 'group-15-stagehand-alpha-pydantic-v1' and extra == 'group-15-stagehand-alpha-pydantic-v2')" }, { name = "attrs" }, { name = "frozenlist" }, { name = "multidict" }, @@ -167,7 +167,7 @@ version = "1.4.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "frozenlist" }, - { name = "typing-extensions", marker = "python_full_version < '3.13' or (extra == 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2')" }, + { name = "typing-extensions", marker = "python_full_version < '3.13' or (extra == 'group-15-stagehand-alpha-pydantic-v1' and extra == 'group-15-stagehand-alpha-pydantic-v2')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/61/62/06741b579156360248d1ec624842ad0edf697050bbaf7c3e46394e106ad1/aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7", size = 25007, upload-time = "2025-07-03T22:54:43.528Z" } wheels = [ @@ -188,9 +188,9 @@ name = "anyio" version = "4.12.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "exceptiongroup", marker = "python_full_version < '3.11' or (extra == 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2')" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11' or (extra == 'group-15-stagehand-alpha-pydantic-v1' and extra == 'group-15-stagehand-alpha-pydantic-v2')" }, { name = "idna" }, - { name = "typing-extensions", marker = "python_full_version < '3.13' or (extra == 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2')" }, + { name = "typing-extensions", marker = "python_full_version < '3.13' or (extra == 'group-15-stagehand-alpha-pydantic-v1' and extra == 'group-15-stagehand-alpha-pydantic-v2')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/16/ce/8a777047513153587e5434fd752e89334ac33e379aa3497db860eeb60377/anyio-4.12.0.tar.gz", hash = "sha256:73c693b567b0c55130c104d0b43a9baf3aa6a31fc6110116509f27bf75e21ec0", size = 228266, upload-time = "2025-11-28T23:37:38.911Z" } wheels = [ @@ -265,7 +265,7 @@ name = "exceptiongroup" version = "1.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions", marker = "python_full_version < '3.13' or (extra == 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2')" }, + { name = "typing-extensions", marker = "python_full_version < '3.13' or (extra == 'group-15-stagehand-alpha-pydantic-v1' and extra == 'group-15-stagehand-alpha-pydantic-v2')" }, ] 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 = [ @@ -506,10 +506,10 @@ name = "iniconfig" version = "2.3.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.14' and extra != 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2'", - "python_full_version >= '3.10' and python_full_version < '3.14' and extra != 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2'", - "python_full_version >= '3.10' and extra == 'group-9-stagehand-pydantic-v1' and extra != 'group-9-stagehand-pydantic-v2'", - "python_full_version >= '3.10' and extra != 'group-9-stagehand-pydantic-v1' and extra != 'group-9-stagehand-pydantic-v2'", + "python_full_version >= '3.14' and extra != 'group-15-stagehand-alpha-pydantic-v1' and extra == 'group-15-stagehand-alpha-pydantic-v2'", + "python_full_version >= '3.10' and python_full_version < '3.14' and extra != 'group-15-stagehand-alpha-pydantic-v1' and extra == 'group-15-stagehand-alpha-pydantic-v2'", + "python_full_version >= '3.10' and extra == 'group-15-stagehand-alpha-pydantic-v1' and extra != 'group-15-stagehand-alpha-pydantic-v2'", + "python_full_version >= '3.10' and extra != 'group-15-stagehand-alpha-pydantic-v1' and extra != 'group-15-stagehand-alpha-pydantic-v2'", ] sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } wheels = [ @@ -524,7 +524,7 @@ resolution-markers = [ "python_full_version < '3.10'", ] dependencies = [ - { name = "mdurl", marker = "python_full_version < '3.10' or (extra == 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2')" }, + { name = "mdurl", marker = "python_full_version < '3.10' or (extra == 'group-15-stagehand-alpha-pydantic-v1' and extra == 'group-15-stagehand-alpha-pydantic-v2')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596, upload-time = "2023-06-03T06:41:14.443Z" } wheels = [ @@ -536,13 +536,13 @@ name = "markdown-it-py" version = "4.0.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.14' and extra != 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2'", - "python_full_version >= '3.10' and python_full_version < '3.14' and extra != 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2'", - "python_full_version >= '3.10' and extra == 'group-9-stagehand-pydantic-v1' and extra != 'group-9-stagehand-pydantic-v2'", - "python_full_version >= '3.10' and extra != 'group-9-stagehand-pydantic-v1' and extra != 'group-9-stagehand-pydantic-v2'", + "python_full_version >= '3.14' and extra != 'group-15-stagehand-alpha-pydantic-v1' and extra == 'group-15-stagehand-alpha-pydantic-v2'", + "python_full_version >= '3.10' and python_full_version < '3.14' and extra != 'group-15-stagehand-alpha-pydantic-v1' and extra == 'group-15-stagehand-alpha-pydantic-v2'", + "python_full_version >= '3.10' and extra == 'group-15-stagehand-alpha-pydantic-v1' and extra != 'group-15-stagehand-alpha-pydantic-v2'", + "python_full_version >= '3.10' and extra != 'group-15-stagehand-alpha-pydantic-v1' and extra != 'group-15-stagehand-alpha-pydantic-v2'", ] dependencies = [ - { name = "mdurl", marker = "python_full_version >= '3.10' or (extra == 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2')" }, + { name = "mdurl", marker = "python_full_version >= '3.10' or (extra == 'group-15-stagehand-alpha-pydantic-v1' and extra == 'group-15-stagehand-alpha-pydantic-v2')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" } wheels = [ @@ -563,7 +563,7 @@ name = "multidict" version = "6.7.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions", marker = "python_full_version < '3.11' or (extra == 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2')" }, + { name = "typing-extensions", marker = "python_full_version < '3.11' or (extra == 'group-15-stagehand-alpha-pydantic-v1' and extra == 'group-15-stagehand-alpha-pydantic-v2')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/80/1e/5492c365f222f907de1039b91f922b93fa4f764c713ee858d235495d8f50/multidict-6.7.0.tar.gz", hash = "sha256:c6e99d9a65ca282e578dfea819cfa9c0a62b2499d8677392e09feaf305e9e6f5", size = 101834, upload-time = "2025-10-06T14:52:30.657Z" } wheels = [ @@ -721,7 +721,7 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mypy-extensions" }, { name = "pathspec" }, - { name = "tomli", marker = "python_full_version < '3.11' or (extra == 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2')" }, + { name = "tomli", marker = "python_full_version < '3.11' or (extra == 'group-15-stagehand-alpha-pydantic-v1' and extra == 'group-15-stagehand-alpha-pydantic-v2')" }, { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/1e/e3/034322d5a779685218ed69286c32faa505247f1f096251ef66c8fd203b08/mypy-1.17.0.tar.gz", hash = "sha256:e5d7ccc08ba089c06e2f5629c660388ef1fee708444f1dee0b9203fa031dee03", size = 3352114, upload-time = "2025-07-14T20:34:30.181Z" } @@ -942,7 +942,7 @@ resolution-markers = [ "python_full_version < '3.10'", ] dependencies = [ - { name = "typing-extensions", marker = "extra == 'group-9-stagehand-pydantic-v1'" }, + { name = "typing-extensions", marker = "extra == 'group-15-stagehand-alpha-pydantic-v1'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/ae/8d/7b346ed940c3e0f9eee7db9be37915a6dac0d9535d736e2ca47a81a066f3/pydantic-1.10.24.tar.gz", hash = "sha256:7e6d1af1bd3d2312079f28c9baf2aafb4a452a06b50717526e5ac562e37baa53", size = 357314, upload-time = "2025-09-25T01:36:33.065Z" } wheels = [ @@ -989,17 +989,17 @@ name = "pydantic" version = "2.12.5" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.14' and extra != 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2'", - "python_full_version >= '3.10' and python_full_version < '3.14' and extra != 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2'", - "python_full_version < '3.10' and extra != 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2'", - "python_full_version >= '3.10' and extra != 'group-9-stagehand-pydantic-v1' and extra != 'group-9-stagehand-pydantic-v2'", - "python_full_version < '3.10' and extra != 'group-9-stagehand-pydantic-v1' and extra != 'group-9-stagehand-pydantic-v2'", + "python_full_version >= '3.14' and extra != 'group-15-stagehand-alpha-pydantic-v1' and extra == 'group-15-stagehand-alpha-pydantic-v2'", + "python_full_version >= '3.10' and python_full_version < '3.14' and extra != 'group-15-stagehand-alpha-pydantic-v1' and extra == 'group-15-stagehand-alpha-pydantic-v2'", + "python_full_version < '3.10' and extra != 'group-15-stagehand-alpha-pydantic-v1' and extra == 'group-15-stagehand-alpha-pydantic-v2'", + "python_full_version >= '3.10' and extra != 'group-15-stagehand-alpha-pydantic-v1' and extra != 'group-15-stagehand-alpha-pydantic-v2'", + "python_full_version < '3.10' and extra != 'group-15-stagehand-alpha-pydantic-v1' and extra != 'group-15-stagehand-alpha-pydantic-v2'", ] dependencies = [ - { name = "annotated-types", marker = "extra == 'group-9-stagehand-pydantic-v2' or extra != 'group-9-stagehand-pydantic-v1'" }, - { name = "pydantic-core", marker = "extra == 'group-9-stagehand-pydantic-v2' or extra != 'group-9-stagehand-pydantic-v1'" }, - { name = "typing-extensions", marker = "extra == 'group-9-stagehand-pydantic-v2' or extra != 'group-9-stagehand-pydantic-v1'" }, - { name = "typing-inspection", marker = "extra == 'group-9-stagehand-pydantic-v2' or extra != 'group-9-stagehand-pydantic-v1'" }, + { name = "annotated-types", marker = "extra == 'group-15-stagehand-alpha-pydantic-v2' or extra != 'group-15-stagehand-alpha-pydantic-v1'" }, + { name = "pydantic-core", marker = "extra == 'group-15-stagehand-alpha-pydantic-v2' or extra != 'group-15-stagehand-alpha-pydantic-v1'" }, + { name = "typing-extensions", marker = "extra == 'group-15-stagehand-alpha-pydantic-v2' or extra != 'group-15-stagehand-alpha-pydantic-v1'" }, + { name = "typing-inspection", marker = "extra == 'group-15-stagehand-alpha-pydantic-v2' or extra != 'group-15-stagehand-alpha-pydantic-v1'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591, upload-time = "2025-11-26T15:11:46.471Z" } wheels = [ @@ -1011,7 +1011,7 @@ name = "pydantic-core" version = "2.41.5" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions", marker = "extra == 'group-9-stagehand-pydantic-v2' or extra != 'group-9-stagehand-pydantic-v1'" }, + { name = "typing-extensions", marker = "extra == 'group-15-stagehand-alpha-pydantic-v2' or extra != 'group-15-stagehand-alpha-pydantic-v1'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" } wheels = [ @@ -1167,13 +1167,13 @@ resolution-markers = [ "python_full_version < '3.10'", ] dependencies = [ - { name = "colorama", marker = "(python_full_version < '3.10' and sys_platform == 'win32') or (python_full_version >= '3.10' and extra == 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2') or (sys_platform != 'win32' and extra == 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2')" }, - { name = "exceptiongroup", marker = "python_full_version < '3.10' or (extra == 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2')" }, - { name = "iniconfig", version = "2.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10' or (extra == 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2')" }, - { name = "packaging", marker = "python_full_version < '3.10' or (extra == 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2')" }, - { name = "pluggy", marker = "python_full_version < '3.10' or (extra == 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2')" }, - { name = "pygments", marker = "python_full_version < '3.10' or (extra == 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2')" }, - { name = "tomli", marker = "python_full_version < '3.10' or (extra == 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2')" }, + { name = "colorama", marker = "(python_full_version < '3.10' and sys_platform == 'win32') or (python_full_version >= '3.10' and extra == 'group-15-stagehand-alpha-pydantic-v1' and extra == 'group-15-stagehand-alpha-pydantic-v2') or (sys_platform != 'win32' and extra == 'group-15-stagehand-alpha-pydantic-v1' and extra == 'group-15-stagehand-alpha-pydantic-v2')" }, + { name = "exceptiongroup", marker = "python_full_version < '3.10' or (extra == 'group-15-stagehand-alpha-pydantic-v1' and extra == 'group-15-stagehand-alpha-pydantic-v2')" }, + { name = "iniconfig", version = "2.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10' or (extra == 'group-15-stagehand-alpha-pydantic-v1' and extra == 'group-15-stagehand-alpha-pydantic-v2')" }, + { name = "packaging", marker = "python_full_version < '3.10' or (extra == 'group-15-stagehand-alpha-pydantic-v1' and extra == 'group-15-stagehand-alpha-pydantic-v2')" }, + { name = "pluggy", marker = "python_full_version < '3.10' or (extra == 'group-15-stagehand-alpha-pydantic-v1' and extra == 'group-15-stagehand-alpha-pydantic-v2')" }, + { name = "pygments", marker = "python_full_version < '3.10' or (extra == 'group-15-stagehand-alpha-pydantic-v1' and extra == 'group-15-stagehand-alpha-pydantic-v2')" }, + { name = "tomli", marker = "python_full_version < '3.10' or (extra == 'group-15-stagehand-alpha-pydantic-v1' and extra == 'group-15-stagehand-alpha-pydantic-v2')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" } wheels = [ @@ -1185,19 +1185,19 @@ name = "pytest" version = "9.0.1" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.14' and extra != 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2'", - "python_full_version >= '3.10' and python_full_version < '3.14' and extra != 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2'", - "python_full_version >= '3.10' and extra == 'group-9-stagehand-pydantic-v1' and extra != 'group-9-stagehand-pydantic-v2'", - "python_full_version >= '3.10' and extra != 'group-9-stagehand-pydantic-v1' and extra != 'group-9-stagehand-pydantic-v2'", + "python_full_version >= '3.14' and extra != 'group-15-stagehand-alpha-pydantic-v1' and extra == 'group-15-stagehand-alpha-pydantic-v2'", + "python_full_version >= '3.10' and python_full_version < '3.14' and extra != 'group-15-stagehand-alpha-pydantic-v1' and extra == 'group-15-stagehand-alpha-pydantic-v2'", + "python_full_version >= '3.10' and extra == 'group-15-stagehand-alpha-pydantic-v1' and extra != 'group-15-stagehand-alpha-pydantic-v2'", + "python_full_version >= '3.10' and extra != 'group-15-stagehand-alpha-pydantic-v1' and extra != 'group-15-stagehand-alpha-pydantic-v2'", ] dependencies = [ - { name = "colorama", marker = "(python_full_version >= '3.10' and sys_platform == 'win32') or (python_full_version < '3.10' and extra == 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2') or (sys_platform != 'win32' and extra == 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2')" }, - { name = "exceptiongroup", marker = "python_full_version == '3.10.*' or (extra == 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2')" }, - { name = "iniconfig", version = "2.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10' or (extra == 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2')" }, - { name = "packaging", marker = "python_full_version >= '3.10' or (extra == 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2')" }, - { name = "pluggy", marker = "python_full_version >= '3.10' or (extra == 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2')" }, - { name = "pygments", marker = "python_full_version >= '3.10' or (extra == 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2')" }, - { name = "tomli", marker = "python_full_version == '3.10.*' or (extra == 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2')" }, + { name = "colorama", marker = "(python_full_version >= '3.10' and sys_platform == 'win32') or (python_full_version < '3.10' and extra == 'group-15-stagehand-alpha-pydantic-v1' and extra == 'group-15-stagehand-alpha-pydantic-v2') or (sys_platform != 'win32' and extra == 'group-15-stagehand-alpha-pydantic-v1' and extra == 'group-15-stagehand-alpha-pydantic-v2')" }, + { name = "exceptiongroup", marker = "python_full_version == '3.10.*' or (extra == 'group-15-stagehand-alpha-pydantic-v1' and extra == 'group-15-stagehand-alpha-pydantic-v2')" }, + { name = "iniconfig", version = "2.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10' or (extra == 'group-15-stagehand-alpha-pydantic-v1' and extra == 'group-15-stagehand-alpha-pydantic-v2')" }, + { name = "packaging", marker = "python_full_version >= '3.10' or (extra == 'group-15-stagehand-alpha-pydantic-v1' and extra == 'group-15-stagehand-alpha-pydantic-v2')" }, + { name = "pluggy", marker = "python_full_version >= '3.10' or (extra == 'group-15-stagehand-alpha-pydantic-v1' and extra == 'group-15-stagehand-alpha-pydantic-v2')" }, + { name = "pygments", marker = "python_full_version >= '3.10' or (extra == 'group-15-stagehand-alpha-pydantic-v1' and extra == 'group-15-stagehand-alpha-pydantic-v2')" }, + { name = "tomli", marker = "python_full_version == '3.10.*' or (extra == 'group-15-stagehand-alpha-pydantic-v1' and extra == 'group-15-stagehand-alpha-pydantic-v2')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/07/56/f013048ac4bc4c1d9be45afd4ab209ea62822fb1598f40687e6bf45dcea4/pytest-9.0.1.tar.gz", hash = "sha256:3e9c069ea73583e255c3b21cf46b8d3c56f6e3a1a8f6da94ccb0fcf57b9d73c8", size = 1564125, upload-time = "2025-11-12T13:05:09.333Z" } wheels = [ @@ -1212,9 +1212,9 @@ resolution-markers = [ "python_full_version < '3.10'", ] dependencies = [ - { name = "backports-asyncio-runner", marker = "python_full_version < '3.10' or (extra == 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2')" }, - { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10' or (extra == 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2')" }, - { name = "typing-extensions", marker = "python_full_version < '3.10' or (extra == 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2')" }, + { name = "backports-asyncio-runner", marker = "python_full_version < '3.10' or (extra == 'group-15-stagehand-alpha-pydantic-v1' and extra == 'group-15-stagehand-alpha-pydantic-v2')" }, + { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10' or (extra == 'group-15-stagehand-alpha-pydantic-v1' and extra == 'group-15-stagehand-alpha-pydantic-v2')" }, + { name = "typing-extensions", marker = "python_full_version < '3.10' or (extra == 'group-15-stagehand-alpha-pydantic-v1' and extra == 'group-15-stagehand-alpha-pydantic-v2')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/42/86/9e3c5f48f7b7b638b216e4b9e645f54d199d7abbbab7a64a13b4e12ba10f/pytest_asyncio-1.2.0.tar.gz", hash = "sha256:c609a64a2a8768462d0c99811ddb8bd2583c33fd33cf7f21af1c142e824ffb57", size = 50119, upload-time = "2025-09-12T07:33:53.816Z" } wheels = [ @@ -1226,15 +1226,15 @@ name = "pytest-asyncio" version = "1.3.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.14' and extra != 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2'", - "python_full_version >= '3.10' and python_full_version < '3.14' and extra != 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2'", - "python_full_version >= '3.10' and extra == 'group-9-stagehand-pydantic-v1' and extra != 'group-9-stagehand-pydantic-v2'", - "python_full_version >= '3.10' and extra != 'group-9-stagehand-pydantic-v1' and extra != 'group-9-stagehand-pydantic-v2'", + "python_full_version >= '3.14' and extra != 'group-15-stagehand-alpha-pydantic-v1' and extra == 'group-15-stagehand-alpha-pydantic-v2'", + "python_full_version >= '3.10' and python_full_version < '3.14' and extra != 'group-15-stagehand-alpha-pydantic-v1' and extra == 'group-15-stagehand-alpha-pydantic-v2'", + "python_full_version >= '3.10' and extra == 'group-15-stagehand-alpha-pydantic-v1' and extra != 'group-15-stagehand-alpha-pydantic-v2'", + "python_full_version >= '3.10' and extra != 'group-15-stagehand-alpha-pydantic-v1' and extra != 'group-15-stagehand-alpha-pydantic-v2'", ] dependencies = [ - { name = "backports-asyncio-runner", marker = "python_full_version == '3.10.*' or (extra == 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2')" }, - { name = "pytest", version = "9.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10' or (extra == 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2')" }, - { name = "typing-extensions", marker = "(python_full_version >= '3.10' and python_full_version < '3.13') or (python_full_version < '3.10' and extra == 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2') or (python_full_version >= '3.13' and extra == 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2')" }, + { name = "backports-asyncio-runner", marker = "python_full_version == '3.10.*' or (extra == 'group-15-stagehand-alpha-pydantic-v1' and extra == 'group-15-stagehand-alpha-pydantic-v2')" }, + { name = "pytest", version = "9.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10' or (extra == 'group-15-stagehand-alpha-pydantic-v1' and extra == 'group-15-stagehand-alpha-pydantic-v2')" }, + { name = "typing-extensions", marker = "(python_full_version >= '3.10' and python_full_version < '3.13') or (python_full_version < '3.10' and extra == 'group-15-stagehand-alpha-pydantic-v1' and extra == 'group-15-stagehand-alpha-pydantic-v2') or (python_full_version >= '3.13' and extra == 'group-15-stagehand-alpha-pydantic-v1' and extra == 'group-15-stagehand-alpha-pydantic-v2')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/90/2c/8af215c0f776415f3590cac4f9086ccefd6fd463befeae41cd4d3f193e5a/pytest_asyncio-1.3.0.tar.gz", hash = "sha256:d7f52f36d231b80ee124cd216ffb19369aa168fc10095013c6b014a34d3ee9e5", size = 50087, upload-time = "2025-11-10T16:07:47.256Z" } wheels = [ @@ -1247,8 +1247,8 @@ version = "3.8.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "execnet" }, - { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10' or (extra == 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2')" }, - { name = "pytest", version = "9.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10' or (extra == 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2')" }, + { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10' or (extra == 'group-15-stagehand-alpha-pydantic-v1' and extra == 'group-15-stagehand-alpha-pydantic-v2')" }, + { name = "pytest", version = "9.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10' or (extra == 'group-15-stagehand-alpha-pydantic-v1' and extra == 'group-15-stagehand-alpha-pydantic-v2')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/78/b4/439b179d1ff526791eb921115fca8e44e596a13efeda518b9d845a619450/pytest_xdist-3.8.0.tar.gz", hash = "sha256:7e578125ec9bc6050861aa93f2d59f1d8d085595d6551c2c90b6f4fad8d3a9f1", size = 88069, upload-time = "2025-07-01T13:30:59.346Z" } wheels = [ @@ -1260,7 +1260,7 @@ name = "python-dateutil" version = "2.9.0.post0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "six", marker = "python_full_version < '3.10' or (extra == 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2')" }, + { name = "six", marker = "python_full_version < '3.10' or (extra == 'group-15-stagehand-alpha-pydantic-v1' and extra == 'group-15-stagehand-alpha-pydantic-v2')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } wheels = [ @@ -1284,8 +1284,8 @@ name = "rich" version = "14.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "markdown-it-py", version = "3.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10' or (extra == 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2')" }, - { name = "markdown-it-py", version = "4.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10' or (extra == 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2')" }, + { name = "markdown-it-py", version = "3.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10' or (extra == 'group-15-stagehand-alpha-pydantic-v1' and extra == 'group-15-stagehand-alpha-pydantic-v2')" }, + { name = "markdown-it-py", version = "4.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10' or (extra == 'group-15-stagehand-alpha-pydantic-v1' and extra == 'group-15-stagehand-alpha-pydantic-v2')" }, { name = "pygments" }, ] sdist = { url = "https://files.pythonhosted.org/packages/fb/d2/8920e102050a0de7bfabeb4c4614a49248cf8d5d7a8d01885fbb24dc767a/rich-14.2.0.tar.gz", hash = "sha256:73ff50c7c0c1c77c8243079283f4edb376f0f6442433aecb8ce7e6d0b92d1fe4", size = 219990, upload-time = "2025-10-09T14:16:53.064Z" } @@ -1338,15 +1338,15 @@ wheels = [ ] [[package]] -name = "stagehand" +name = "stagehand-alpha" version = "0.0.1" source = { editable = "." } dependencies = [ { name = "anyio" }, { name = "distro" }, { name = "httpx" }, - { name = "pydantic", version = "1.10.24", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'group-9-stagehand-pydantic-v1'" }, - { name = "pydantic", version = "2.12.5", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'group-9-stagehand-pydantic-v2' or extra != 'group-9-stagehand-pydantic-v1'" }, + { name = "pydantic", version = "1.10.24", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'group-15-stagehand-alpha-pydantic-v1'" }, + { name = "pydantic", version = "2.12.5", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'group-15-stagehand-alpha-pydantic-v2' or extra != 'group-15-stagehand-alpha-pydantic-v1'" }, { name = "sniffio" }, { name = "typing-extensions" }, ] @@ -1363,16 +1363,16 @@ dev = [ { name = "importlib-metadata" }, { name = "mypy" }, { name = "pyright" }, - { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10' or (extra == 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2')" }, - { name = "pytest", version = "9.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10' or (extra == 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2')" }, - { name = "pytest-asyncio", version = "1.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10' or (extra == 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2')" }, - { name = "pytest-asyncio", version = "1.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10' or (extra == 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2')" }, + { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10' or (extra == 'group-15-stagehand-alpha-pydantic-v1' and extra == 'group-15-stagehand-alpha-pydantic-v2')" }, + { name = "pytest", version = "9.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10' or (extra == 'group-15-stagehand-alpha-pydantic-v1' and extra == 'group-15-stagehand-alpha-pydantic-v2')" }, + { name = "pytest-asyncio", version = "1.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10' or (extra == 'group-15-stagehand-alpha-pydantic-v1' and extra == 'group-15-stagehand-alpha-pydantic-v2')" }, + { name = "pytest-asyncio", version = "1.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10' or (extra == 'group-15-stagehand-alpha-pydantic-v1' and extra == 'group-15-stagehand-alpha-pydantic-v2')" }, { name = "pytest-xdist" }, { name = "respx" }, { name = "rich" }, { name = "ruff" }, - { name = "time-machine", version = "2.19.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10' or (extra == 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2')" }, - { name = "time-machine", version = "3.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10' or (extra == 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2')" }, + { name = "time-machine", version = "2.19.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10' or (extra == 'group-15-stagehand-alpha-pydantic-v1' and extra == 'group-15-stagehand-alpha-pydantic-v2')" }, + { name = "time-machine", version = "3.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10' or (extra == 'group-15-stagehand-alpha-pydantic-v1' and extra == 'group-15-stagehand-alpha-pydantic-v2')" }, ] pydantic-v1 = [ { name = "pydantic", version = "1.10.24", source = { registry = "https://pypi.org/simple" } }, @@ -1422,7 +1422,7 @@ resolution-markers = [ "python_full_version < '3.10'", ] dependencies = [ - { name = "python-dateutil", marker = "python_full_version < '3.10' or (extra == 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2')" }, + { name = "python-dateutil", marker = "python_full_version < '3.10' or (extra == 'group-15-stagehand-alpha-pydantic-v1' and extra == 'group-15-stagehand-alpha-pydantic-v2')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/f8/a4/1b5fdd165f61b67f445fac2a7feb0c655118edef429cd09ff5a8067f7f1d/time_machine-2.19.0.tar.gz", hash = "sha256:7c5065a8b3f2bbb449422c66ef71d114d3f909c276a6469642ecfffb6a0fcd29", size = 14576, upload-time = "2025-08-19T17:22:08.402Z" } wheels = [ @@ -1521,10 +1521,10 @@ name = "time-machine" version = "3.1.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.14' and extra != 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2'", - "python_full_version >= '3.10' and python_full_version < '3.14' and extra != 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2'", - "python_full_version >= '3.10' and extra == 'group-9-stagehand-pydantic-v1' and extra != 'group-9-stagehand-pydantic-v2'", - "python_full_version >= '3.10' and extra != 'group-9-stagehand-pydantic-v1' and extra != 'group-9-stagehand-pydantic-v2'", + "python_full_version >= '3.14' and extra != 'group-15-stagehand-alpha-pydantic-v1' and extra == 'group-15-stagehand-alpha-pydantic-v2'", + "python_full_version >= '3.10' and python_full_version < '3.14' and extra != 'group-15-stagehand-alpha-pydantic-v1' and extra == 'group-15-stagehand-alpha-pydantic-v2'", + "python_full_version >= '3.10' and extra == 'group-15-stagehand-alpha-pydantic-v1' and extra != 'group-15-stagehand-alpha-pydantic-v2'", + "python_full_version >= '3.10' and extra != 'group-15-stagehand-alpha-pydantic-v1' and extra != 'group-15-stagehand-alpha-pydantic-v2'", ] sdist = { url = "https://files.pythonhosted.org/packages/17/bd/a1bb03eb39ce35c966f0dde6559df7348ca0580f7cd3a956fdd7ed0b5080/time_machine-3.1.0.tar.gz", hash = "sha256:90831c2cf9e18e4199abb85fafa0c0ca0c6c15d0894a03ef68d5005a796c4f27", size = 14436, upload-time = "2025-11-21T13:56:33.802Z" } wheels = [ @@ -1670,7 +1670,7 @@ name = "typing-inspection" version = "0.4.2" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions", marker = "extra == 'group-9-stagehand-pydantic-v2' or extra != 'group-9-stagehand-pydantic-v1'" }, + { name = "typing-extensions", marker = "extra == 'group-15-stagehand-alpha-pydantic-v2' or extra != 'group-15-stagehand-alpha-pydantic-v1'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } wheels = [ From 981620ceb3f47cfef935f4e9b29b7fa62e42e01a Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 23 Dec 2025 20:15:08 +0000 Subject: [PATCH 48/88] chore(internal): version bump --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/stagehand/_version.py | 2 +- uv.lock | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 1332969b..3d2ac0bd 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.0.1" + ".": "0.1.0" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 4e081fd3..eeb79abe 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "stagehand-alpha" -version = "0.0.1" +version = "0.1.0" description = "The official Python library for the stagehand API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/stagehand/_version.py b/src/stagehand/_version.py index e1b7ed63..a094f3ed 100644 --- a/src/stagehand/_version.py +++ b/src/stagehand/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "stagehand" -__version__ = "0.0.1" # x-release-please-version +__version__ = "0.1.0" # x-release-please-version diff --git a/uv.lock b/uv.lock index 4e5706cb..f6783426 100644 --- a/uv.lock +++ b/uv.lock @@ -1339,7 +1339,7 @@ wheels = [ [[package]] name = "stagehand-alpha" -version = "0.0.1" +version = "0.1.0" source = { editable = "." } dependencies = [ { name = "anyio" }, From 4106b6d07c2f287d42ad76ee2946f73b628534e4 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 5 Jan 2026 21:38:03 +0000 Subject: [PATCH 49/88] feat(api): manual updates --- .stats.yml | 2 +- LICENSE | 2 +- src/stagehand/_client.py | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.stats.yml b/.stats.yml index 5a89fb75..3a111788 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 7 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fstagehand-ed52466945f2f8dfd3814a29e948d7bf30af7b76a7a7689079c03b8baf64e26f.yml openapi_spec_hash: 5d57aaf2362b0d882372dbf76477ba23 -config_hash: 989ddfee371586e9156b4d484ec0a6cc +config_hash: 8ec9eaf59304f664cf79f73de1488671 diff --git a/LICENSE b/LICENSE index 6b24314a..d15d0212 100644 --- a/LICENSE +++ b/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright 2025 Stagehand + Copyright 2026 Stagehand Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/stagehand/_client.py b/src/stagehand/_client.py index 9f93e242..f5c0ea12 100644 --- a/src/stagehand/_client.py +++ b/src/stagehand/_client.py @@ -111,7 +111,7 @@ def __init__( if base_url is None: base_url = os.environ.get("STAGEHAND_BASE_URL") if base_url is None: - base_url = f"https://api.stagehand.browserbase.com/v1" + base_url = f"https://api.stagehand.browserbase.com" super().__init__( version=__version__, @@ -328,7 +328,7 @@ def __init__( if base_url is None: base_url = os.environ.get("STAGEHAND_BASE_URL") if base_url is None: - base_url = f"https://api.stagehand.browserbase.com/v1" + base_url = f"https://api.stagehand.browserbase.com" super().__init__( version=__version__, From f914b039d68536c8bcbb17c844033fd9996382c5 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 6 Jan 2026 03:46:04 +0000 Subject: [PATCH 50/88] codegen metadata --- .stats.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.stats.yml b/.stats.yml index 3a111788..19154164 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 7 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fstagehand-ed52466945f2f8dfd3814a29e948d7bf30af7b76a7a7689079c03b8baf64e26f.yml -openapi_spec_hash: 5d57aaf2362b0d882372dbf76477ba23 -config_hash: 8ec9eaf59304f664cf79f73de1488671 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fstagehand-2c6c017cc9ca1fcfe7b3902edfa64fb0420bdb46b1740c7c862e81e2132d4f7c.yml +openapi_spec_hash: 220daf7e8f5897909a6c10e3385386e3 +config_hash: 1f709f8775e13029dc60064ef3a94355 From a9fe1503411c3926741eb9c6d4f3e19c35a38169 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 6 Jan 2026 05:02:48 +0000 Subject: [PATCH 51/88] docs: prominently feature MCP server setup in root SDK readmes --- README.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/README.md b/README.md index 6527e46a..fdecf96a 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,15 @@ and offers both synchronous and asynchronous clients powered by [httpx](https:// It is generated with [Stainless](https://www.stainless.com/). +## MCP Server + +Use the Stagehand MCP Server to enable AI assistants to interact with this API, allowing them to explore endpoints, make test requests, and use documentation to help integrate this SDK into your application. + +[![Add to Cursor](https://cursor.com/deeplink/mcp-install-dark.svg)](https://cursor.com/en-US/install-mcp?name=stagehand-mcp&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsInN0YWdlaGFuZC1tY3AiXX0) +[![Install in VS Code](https://img.shields.io/badge/_-Add_to_VS_Code-blue?style=for-the-badge&logo=data:image/svg%2bxml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGZpbGw9Im5vbmUiIHZpZXdCb3g9IjAgMCA0MCA0MCI+PHBhdGggZmlsbD0iI0VFRSIgZmlsbC1ydWxlPSJldmVub2RkIiBkPSJNMzAuMjM1IDM5Ljg4NGEyLjQ5MSAyLjQ5MSAwIDAgMS0xLjc4MS0uNzNMMTIuNyAyNC43OGwtMy40NiAyLjYyNC0zLjQwNiAyLjU4MmExLjY2NSAxLjY2NSAwIDAgMS0xLjA4Mi4zMzggMS42NjQgMS42NjQgMCAwIDEtMS4wNDYtLjQzMWwtMi4yLTJhMS42NjYgMS42NjYgMCAwIDEgMC0yLjQ2M0w3LjQ1OCAyMCA0LjY3IDE3LjQ1MyAxLjUwNyAxNC41N2ExLjY2NSAxLjY2NSAwIDAgMSAwLTIuNDYzbDIuMi0yYTEuNjY1IDEuNjY1IDAgMCAxIDIuMTMtLjA5N2w2Ljg2MyA1LjIwOUwyOC40NTIuODQ0YTIuNDg4IDIuNDg4IDAgMCAxIDEuODQxLS43MjljLjM1MS4wMDkuNjk5LjA5MSAxLjAxOS4yNDVsOC4yMzYgMy45NjFhMi41IDIuNSAwIDAgMSAxLjQxNSAyLjI1M3YuMDk5LS4wNDVWMzMuMzd2LS4wNDUuMDk1YTIuNTAxIDIuNTAxIDAgMCAxLTEuNDE2IDIuMjU3bC04LjIzNSAzLjk2MWEyLjQ5MiAyLjQ5MiAwIDAgMS0xLjA3Ny4yNDZabS43MTYtMjguOTQ3LTExLjk0OCA5LjA2MiAxMS45NTIgOS4wNjUtLjAwNC0xOC4xMjdaIi8+PC9zdmc+)](https://vscode.stainless.com/mcp/%7B%22name%22%3A%22stagehand-mcp%22%2C%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22stagehand-mcp%22%5D%7D) + +> Note: You may need to set environment variables in your MCP client. + ## Documentation The REST API documentation can be found on [docs.stagehand.dev](https://docs.stagehand.dev). The full API of this library can be found in [api.md](api.md). From 9d7baf54277eb82f4fe4fab5f85550e71a6cad61 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 6 Jan 2026 19:34:09 +0000 Subject: [PATCH 52/88] feat: Added optional param to force empty object --- .stats.yml | 4 ++-- api.md | 2 +- src/stagehand/resources/sessions.py | 5 +++++ src/stagehand/types/__init__.py | 1 + src/stagehand/types/session_end_params.py | 27 +++++++++++++++++++++++ tests/api_resources/test_sessions.py | 2 ++ 6 files changed, 38 insertions(+), 3 deletions(-) create mode 100644 src/stagehand/types/session_end_params.py diff --git a/.stats.yml b/.stats.yml index 19154164..f4f95e84 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 7 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fstagehand-2c6c017cc9ca1fcfe7b3902edfa64fb0420bdb46b1740c7c862e81e2132d4f7c.yml -openapi_spec_hash: 220daf7e8f5897909a6c10e3385386e3 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fstagehand-39cd9547d16412cf0568f6ce2ad8d43805dffe65bde830beeff630b903ae3b38.yml +openapi_spec_hash: 9cd7c9fefa686f9711392782d948470f config_hash: 1f709f8775e13029dc60064ef3a94355 diff --git a/api.md b/api.md index d5ec2705..dae59737 100644 --- a/api.md +++ b/api.md @@ -20,7 +20,7 @@ from stagehand.types import ( Methods: - client.sessions.act(id, \*\*params) -> SessionActResponse -- client.sessions.end(id) -> SessionEndResponse +- client.sessions.end(id, \*\*params) -> SessionEndResponse - client.sessions.execute(id, \*\*params) -> SessionExecuteResponse - client.sessions.extract(id, \*\*params) -> SessionExtractResponse - client.sessions.navigate(id, \*\*params) -> SessionNavigateResponse diff --git a/src/stagehand/resources/sessions.py b/src/stagehand/resources/sessions.py index 19fe9256..dfdcd7b8 100644 --- a/src/stagehand/resources/sessions.py +++ b/src/stagehand/resources/sessions.py @@ -10,6 +10,7 @@ from ..types import ( session_act_params, + session_end_params, session_start_params, session_execute_params, session_extract_params, @@ -271,6 +272,7 @@ def end( self, id: str, *, + _force_body: object | Omit = omit, x_language: Literal["typescript", "python", "playground"] | Omit = omit, x_sdk_version: str | Omit = omit, x_sent_at: Union[str, datetime] | Omit = omit, @@ -319,6 +321,7 @@ def end( } return self._post( f"/v1/sessions/{id}/end", + body=maybe_transform({"_force_body": _force_body}, session_end_params.SessionEndParams), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -1350,6 +1353,7 @@ async def end( self, id: str, *, + _force_body: object | Omit = omit, x_language: Literal["typescript", "python", "playground"] | Omit = omit, x_sdk_version: str | Omit = omit, x_sent_at: Union[str, datetime] | Omit = omit, @@ -1398,6 +1402,7 @@ async def end( } return await self._post( f"/v1/sessions/{id}/end", + body=await async_maybe_transform({"_force_body": _force_body}, session_end_params.SessionEndParams), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), diff --git a/src/stagehand/types/__init__.py b/src/stagehand/types/__init__.py index 46ce9d80..145b9be0 100644 --- a/src/stagehand/types/__init__.py +++ b/src/stagehand/types/__init__.py @@ -6,6 +6,7 @@ from .stream_event import StreamEvent as StreamEvent from .model_config_param import ModelConfigParam as ModelConfigParam from .session_act_params import SessionActParams as SessionActParams +from .session_end_params import SessionEndParams as SessionEndParams from .session_act_response import SessionActResponse as SessionActResponse from .session_end_response import SessionEndResponse as SessionEndResponse from .session_start_params import SessionStartParams as SessionStartParams diff --git a/src/stagehand/types/session_end_params.py b/src/stagehand/types/session_end_params.py new file mode 100644 index 00000000..defdf60a --- /dev/null +++ b/src/stagehand/types/session_end_params.py @@ -0,0 +1,27 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Union +from datetime import datetime +from typing_extensions import Literal, Annotated, TypedDict + +from .._utils import PropertyInfo + +__all__ = ["SessionEndParams"] + + +class SessionEndParams(TypedDict, total=False): + _force_body: Annotated[object, PropertyInfo(alias="_forceBody")] + + x_language: Annotated[Literal["typescript", "python", "playground"], PropertyInfo(alias="x-language")] + """Client SDK language""" + + x_sdk_version: Annotated[str, PropertyInfo(alias="x-sdk-version")] + """Version of the Stagehand SDK""" + + x_sent_at: Annotated[Union[str, datetime], PropertyInfo(alias="x-sent-at", format="iso8601")] + """ISO timestamp when request was sent""" + + x_stream_response: Annotated[Literal["true", "false"], PropertyInfo(alias="x-stream-response")] + """Whether to stream the response via SSE""" diff --git a/tests/api_resources/test_sessions.py b/tests/api_resources/test_sessions.py index daef296d..f4d73556 100644 --- a/tests/api_resources/test_sessions.py +++ b/tests/api_resources/test_sessions.py @@ -174,6 +174,7 @@ def test_method_end(self, client: Stagehand) -> None: def test_method_end_with_all_params(self, client: Stagehand) -> None: session = client.sessions.end( id="c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123", + _force_body={}, x_language="typescript", x_sdk_version="3.0.6", x_sent_at=parse_datetime("2025-01-15T10:30:00Z"), @@ -984,6 +985,7 @@ async def test_method_end(self, async_client: AsyncStagehand) -> None: async def test_method_end_with_all_params(self, async_client: AsyncStagehand) -> None: session = await async_client.sessions.end( id="c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123", + _force_body={}, x_language="typescript", x_sdk_version="3.0.6", x_sent_at=parse_datetime("2025-01-15T10:30:00Z"), From 5d09a7051737a332ed0a6697d7dc6ebb5c0ad3fb Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 7 Jan 2026 01:53:52 +0000 Subject: [PATCH 53/88] chore(internal): version bump --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/stagehand/_version.py | 2 +- uv.lock | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 3d2ac0bd..10f30916 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.1.0" + ".": "0.2.0" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index eeb79abe..1b95b953 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "stagehand-alpha" -version = "0.1.0" +version = "0.2.0" description = "The official Python library for the stagehand API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/stagehand/_version.py b/src/stagehand/_version.py index a094f3ed..5772616e 100644 --- a/src/stagehand/_version.py +++ b/src/stagehand/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "stagehand" -__version__ = "0.1.0" # x-release-please-version +__version__ = "0.2.0" # x-release-please-version diff --git a/uv.lock b/uv.lock index f6783426..b65ca834 100644 --- a/uv.lock +++ b/uv.lock @@ -1339,7 +1339,7 @@ wheels = [ [[package]] name = "stagehand-alpha" -version = "0.1.0" +version = "0.2.0" source = { editable = "." } dependencies = [ { name = "anyio" }, From 27bce19c4d8d2eeb10ad427a09194b83817c401a Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 7 Jan 2026 02:32:53 +0000 Subject: [PATCH 54/88] chore(internal): version bump --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/stagehand/_version.py | 2 +- uv.lock | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 10f30916..b06ba919 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.2.0" + ".": "0.2.1" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 1b95b953..10982aeb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "stagehand-alpha" -version = "0.2.0" +version = "0.2.1" description = "The official Python library for the stagehand API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/stagehand/_version.py b/src/stagehand/_version.py index 5772616e..8bc3b5ea 100644 --- a/src/stagehand/_version.py +++ b/src/stagehand/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "stagehand" -__version__ = "0.2.0" # x-release-please-version +__version__ = "0.2.1" # x-release-please-version diff --git a/uv.lock b/uv.lock index b65ca834..3d16f54b 100644 --- a/uv.lock +++ b/uv.lock @@ -1339,7 +1339,7 @@ wheels = [ [[package]] name = "stagehand-alpha" -version = "0.2.0" +version = "0.2.1" source = { editable = "." } dependencies = [ { name = "anyio" }, From bbbd73f83f1095e7819128aad2a0d62a44654c37 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 7 Jan 2026 02:55:55 +0000 Subject: [PATCH 55/88] chore(internal): version bump --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/stagehand/_version.py | 2 +- uv.lock | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index b06ba919..d66ca57c 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.2.1" + ".": "0.2.2" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 10982aeb..8a34767c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "stagehand-alpha" -version = "0.2.1" +version = "0.2.2" description = "The official Python library for the stagehand API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/stagehand/_version.py b/src/stagehand/_version.py index 8bc3b5ea..bdba5bc0 100644 --- a/src/stagehand/_version.py +++ b/src/stagehand/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "stagehand" -__version__ = "0.2.1" # x-release-please-version +__version__ = "0.2.2" # x-release-please-version diff --git a/uv.lock b/uv.lock index 3d16f54b..ce7d3ba5 100644 --- a/uv.lock +++ b/uv.lock @@ -1339,7 +1339,7 @@ wheels = [ [[package]] name = "stagehand-alpha" -version = "0.2.1" +version = "0.2.2" source = { editable = "." } dependencies = [ { name = "anyio" }, From 3103b6d1116b4f851ca4d871651662ec55a1c95c Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 7 Jan 2026 03:15:46 +0000 Subject: [PATCH 56/88] chore(internal): version bump --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/stagehand/_version.py | 2 +- uv.lock | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index d66ca57c..ccdf8aa7 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.2.2" + ".": "0.2.3" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 8a34767c..b8ab3acd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "stagehand-alpha" -version = "0.2.2" +version = "0.2.3" description = "The official Python library for the stagehand API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/stagehand/_version.py b/src/stagehand/_version.py index bdba5bc0..a6cd167b 100644 --- a/src/stagehand/_version.py +++ b/src/stagehand/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "stagehand" -__version__ = "0.2.2" # x-release-please-version +__version__ = "0.2.3" # x-release-please-version diff --git a/uv.lock b/uv.lock index ce7d3ba5..dc814a30 100644 --- a/uv.lock +++ b/uv.lock @@ -1339,7 +1339,7 @@ wheels = [ [[package]] name = "stagehand-alpha" -version = "0.2.2" +version = "0.2.3" source = { editable = "." } dependencies = [ { name = "anyio" }, From 1832012755363f49f4fe6c5fe76da5fdfb8283b6 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 7 Jan 2026 20:47:25 +0000 Subject: [PATCH 57/88] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index f4f95e84..b120b664 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 7 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fstagehand-39cd9547d16412cf0568f6ce2ad8d43805dffe65bde830beeff630b903ae3b38.yml openapi_spec_hash: 9cd7c9fefa686f9711392782d948470f -config_hash: 1f709f8775e13029dc60064ef3a94355 +config_hash: 3c21550e2c94cad4339d3093d794beb0 From 38840f955e9ae6b5368d376ce780851c7cbee314 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 7 Jan 2026 23:29:14 +0000 Subject: [PATCH 58/88] chore(internal): version bump --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/stagehand/_version.py | 2 +- uv.lock | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index ccdf8aa7..9aa540f7 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.2.3" + ".": "0.2.4" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index b8ab3acd..5be108ae 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "stagehand-alpha" -version = "0.2.3" +version = "0.2.4" description = "The official Python library for the stagehand API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/stagehand/_version.py b/src/stagehand/_version.py index a6cd167b..f26dd6ac 100644 --- a/src/stagehand/_version.py +++ b/src/stagehand/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "stagehand" -__version__ = "0.2.3" # x-release-please-version +__version__ = "0.2.4" # x-release-please-version diff --git a/uv.lock b/uv.lock index dc814a30..512dee99 100644 --- a/uv.lock +++ b/uv.lock @@ -1339,7 +1339,7 @@ wheels = [ [[package]] name = "stagehand-alpha" -version = "0.2.3" +version = "0.2.4" source = { editable = "." } dependencies = [ { name = "anyio" }, From a9580862ff4d4ed9f81dc579efa7a0a41c7ceaeb Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 10 Jan 2026 00:00:36 +0000 Subject: [PATCH 59/88] feat: Removed requiring x-language and x-sdk-version from openapi spec --- .stats.yml | 4 +- src/stagehand/resources/sessions.py | 224 ------------------ src/stagehand/types/session_act_params.py | 6 - src/stagehand/types/session_end_params.py | 6 - src/stagehand/types/session_execute_params.py | 6 - src/stagehand/types/session_extract_params.py | 6 - .../types/session_navigate_params.py | 6 - src/stagehand/types/session_observe_params.py | 6 - src/stagehand/types/session_start_params.py | 6 - tests/api_resources/test_sessions.py | 44 ---- 10 files changed, 2 insertions(+), 312 deletions(-) diff --git a/.stats.yml b/.stats.yml index b120b664..f53d855f 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 7 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fstagehand-39cd9547d16412cf0568f6ce2ad8d43805dffe65bde830beeff630b903ae3b38.yml -openapi_spec_hash: 9cd7c9fefa686f9711392782d948470f +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fstagehand-1705ff86e7ec80d6be2ddbb0e3cbee821f3e95d68fa6a48c790f586e3470e678.yml +openapi_spec_hash: cf0d4dad078a7f7c1256b437e349b911 config_hash: 3c21550e2c94cad4339d3093d794beb0 diff --git a/src/stagehand/resources/sessions.py b/src/stagehand/resources/sessions.py index dfdcd7b8..5f31c6b9 100644 --- a/src/stagehand/resources/sessions.py +++ b/src/stagehand/resources/sessions.py @@ -70,8 +70,6 @@ def act( frame_id: str | Omit = omit, options: session_act_params.Options | Omit = omit, stream_response: Literal[False] | Omit = omit, - x_language: Literal["typescript", "python", "playground"] | Omit = omit, - x_sdk_version: str | Omit = omit, x_sent_at: Union[str, datetime] | Omit = omit, x_stream_response: Literal["true", "false"] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. @@ -94,10 +92,6 @@ def act( stream_response: Whether to stream the response via SSE - x_language: Client SDK language - - x_sdk_version: Version of the Stagehand SDK - x_sent_at: ISO timestamp when request was sent x_stream_response: Whether to stream the response via SSE @@ -121,8 +115,6 @@ def act( stream_response: Literal[True], frame_id: str | Omit = omit, options: session_act_params.Options | Omit = omit, - x_language: Literal["typescript", "python", "playground"] | Omit = omit, - x_sdk_version: str | Omit = omit, x_sent_at: Union[str, datetime] | Omit = omit, x_stream_response: Literal["true", "false"] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. @@ -145,10 +137,6 @@ def act( frame_id: Target frame ID for the action - x_language: Client SDK language - - x_sdk_version: Version of the Stagehand SDK - x_sent_at: ISO timestamp when request was sent x_stream_response: Whether to stream the response via SSE @@ -172,8 +160,6 @@ def act( stream_response: bool, frame_id: str | Omit = omit, options: session_act_params.Options | Omit = omit, - x_language: Literal["typescript", "python", "playground"] | Omit = omit, - x_sdk_version: str | Omit = omit, x_sent_at: Union[str, datetime] | Omit = omit, x_stream_response: Literal["true", "false"] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. @@ -196,10 +182,6 @@ def act( frame_id: Target frame ID for the action - x_language: Client SDK language - - x_sdk_version: Version of the Stagehand SDK - x_sent_at: ISO timestamp when request was sent x_stream_response: Whether to stream the response via SSE @@ -223,8 +205,6 @@ def act( frame_id: str | Omit = omit, options: session_act_params.Options | Omit = omit, stream_response: Literal[False] | Literal[True] | Omit = omit, - x_language: Literal["typescript", "python", "playground"] | Omit = omit, - x_sdk_version: str | Omit = omit, x_sent_at: Union[str, datetime] | Omit = omit, x_stream_response: Literal["true", "false"] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. @@ -239,8 +219,6 @@ def act( extra_headers = { **strip_not_given( { - "x-language": str(x_language) if is_given(x_language) else not_given, - "x-sdk-version": x_sdk_version, "x-sent-at": x_sent_at.isoformat() if is_given(x_sent_at) else not_given, "x-stream-response": str(x_stream_response) if is_given(x_stream_response) else not_given, } @@ -273,8 +251,6 @@ def end( id: str, *, _force_body: object | Omit = omit, - x_language: Literal["typescript", "python", "playground"] | Omit = omit, - x_sdk_version: str | Omit = omit, x_sent_at: Union[str, datetime] | Omit = omit, x_stream_response: Literal["true", "false"] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. @@ -290,10 +266,6 @@ def end( Args: id: Unique session identifier - x_language: Client SDK language - - x_sdk_version: Version of the Stagehand SDK - x_sent_at: ISO timestamp when request was sent x_stream_response: Whether to stream the response via SSE @@ -311,8 +283,6 @@ def end( extra_headers = { **strip_not_given( { - "x-language": str(x_language) if is_given(x_language) else not_given, - "x-sdk-version": x_sdk_version, "x-sent-at": x_sent_at.isoformat() if is_given(x_sent_at) else not_given, "x-stream-response": str(x_stream_response) if is_given(x_stream_response) else not_given, } @@ -337,8 +307,6 @@ def execute( execute_options: session_execute_params.ExecuteOptions, frame_id: str | Omit = omit, stream_response: Literal[False] | Omit = omit, - x_language: Literal["typescript", "python", "playground"] | Omit = omit, - x_sdk_version: str | Omit = omit, x_sent_at: Union[str, datetime] | Omit = omit, x_stream_response: Literal["true", "false"] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. @@ -358,10 +326,6 @@ def execute( stream_response: Whether to stream the response via SSE - x_language: Client SDK language - - x_sdk_version: Version of the Stagehand SDK - x_sent_at: ISO timestamp when request was sent x_stream_response: Whether to stream the response via SSE @@ -385,8 +349,6 @@ def execute( execute_options: session_execute_params.ExecuteOptions, stream_response: Literal[True], frame_id: str | Omit = omit, - x_language: Literal["typescript", "python", "playground"] | Omit = omit, - x_sdk_version: str | Omit = omit, x_sent_at: Union[str, datetime] | Omit = omit, x_stream_response: Literal["true", "false"] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. @@ -406,10 +368,6 @@ def execute( frame_id: Target frame ID for the agent - x_language: Client SDK language - - x_sdk_version: Version of the Stagehand SDK - x_sent_at: ISO timestamp when request was sent x_stream_response: Whether to stream the response via SSE @@ -433,8 +391,6 @@ def execute( execute_options: session_execute_params.ExecuteOptions, stream_response: bool, frame_id: str | Omit = omit, - x_language: Literal["typescript", "python", "playground"] | Omit = omit, - x_sdk_version: str | Omit = omit, x_sent_at: Union[str, datetime] | Omit = omit, x_stream_response: Literal["true", "false"] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. @@ -454,10 +410,6 @@ def execute( frame_id: Target frame ID for the agent - x_language: Client SDK language - - x_sdk_version: Version of the Stagehand SDK - x_sent_at: ISO timestamp when request was sent x_stream_response: Whether to stream the response via SSE @@ -481,8 +433,6 @@ def execute( execute_options: session_execute_params.ExecuteOptions, frame_id: str | Omit = omit, stream_response: Literal[False] | Literal[True] | Omit = omit, - x_language: Literal["typescript", "python", "playground"] | Omit = omit, - x_sdk_version: str | Omit = omit, x_sent_at: Union[str, datetime] | Omit = omit, x_stream_response: Literal["true", "false"] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. @@ -497,8 +447,6 @@ def execute( extra_headers = { **strip_not_given( { - "x-language": str(x_language) if is_given(x_language) else not_given, - "x-sdk-version": x_sdk_version, "x-sent-at": x_sent_at.isoformat() if is_given(x_sent_at) else not_given, "x-stream-response": str(x_stream_response) if is_given(x_stream_response) else not_given, } @@ -536,8 +484,6 @@ def extract( options: session_extract_params.Options | Omit = omit, schema: Dict[str, object] | Omit = omit, stream_response: Literal[False] | Omit = omit, - x_language: Literal["typescript", "python", "playground"] | Omit = omit, - x_sdk_version: str | Omit = omit, x_sent_at: Union[str, datetime] | Omit = omit, x_stream_response: Literal["true", "false"] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. @@ -561,10 +507,6 @@ def extract( stream_response: Whether to stream the response via SSE - x_language: Client SDK language - - x_sdk_version: Version of the Stagehand SDK - x_sent_at: ISO timestamp when request was sent x_stream_response: Whether to stream the response via SSE @@ -589,8 +531,6 @@ def extract( instruction: str | Omit = omit, options: session_extract_params.Options | Omit = omit, schema: Dict[str, object] | Omit = omit, - x_language: Literal["typescript", "python", "playground"] | Omit = omit, - x_sdk_version: str | Omit = omit, x_sent_at: Union[str, datetime] | Omit = omit, x_stream_response: Literal["true", "false"] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. @@ -614,10 +554,6 @@ def extract( schema: JSON Schema defining the structure of data to extract - x_language: Client SDK language - - x_sdk_version: Version of the Stagehand SDK - x_sent_at: ISO timestamp when request was sent x_stream_response: Whether to stream the response via SSE @@ -642,8 +578,6 @@ def extract( instruction: str | Omit = omit, options: session_extract_params.Options | Omit = omit, schema: Dict[str, object] | Omit = omit, - x_language: Literal["typescript", "python", "playground"] | Omit = omit, - x_sdk_version: str | Omit = omit, x_sent_at: Union[str, datetime] | Omit = omit, x_stream_response: Literal["true", "false"] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. @@ -667,10 +601,6 @@ def extract( schema: JSON Schema defining the structure of data to extract - x_language: Client SDK language - - x_sdk_version: Version of the Stagehand SDK - x_sent_at: ISO timestamp when request was sent x_stream_response: Whether to stream the response via SSE @@ -694,8 +624,6 @@ def extract( options: session_extract_params.Options | Omit = omit, schema: Dict[str, object] | Omit = omit, stream_response: Literal[False] | Literal[True] | Omit = omit, - x_language: Literal["typescript", "python", "playground"] | Omit = omit, - x_sdk_version: str | Omit = omit, x_sent_at: Union[str, datetime] | Omit = omit, x_stream_response: Literal["true", "false"] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. @@ -710,8 +638,6 @@ def extract( extra_headers = { **strip_not_given( { - "x-language": str(x_language) if is_given(x_language) else not_given, - "x-sdk-version": x_sdk_version, "x-sent-at": x_sent_at.isoformat() if is_given(x_sent_at) else not_given, "x-stream-response": str(x_stream_response) if is_given(x_stream_response) else not_given, } @@ -748,8 +674,6 @@ def navigate( frame_id: str | Omit = omit, options: session_navigate_params.Options | Omit = omit, stream_response: bool | Omit = omit, - x_language: Literal["typescript", "python", "playground"] | Omit = omit, - x_sdk_version: str | Omit = omit, x_sent_at: Union[str, datetime] | Omit = omit, x_stream_response: Literal["true", "false"] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. @@ -771,10 +695,6 @@ def navigate( stream_response: Whether to stream the response via SSE - x_language: Client SDK language - - x_sdk_version: Version of the Stagehand SDK - x_sent_at: ISO timestamp when request was sent x_stream_response: Whether to stream the response via SSE @@ -792,8 +712,6 @@ def navigate( extra_headers = { **strip_not_given( { - "x-language": str(x_language) if is_given(x_language) else not_given, - "x-sdk-version": x_sdk_version, "x-sent-at": x_sent_at.isoformat() if is_given(x_sent_at) else not_given, "x-stream-response": str(x_stream_response) if is_given(x_stream_response) else not_given, } @@ -826,8 +744,6 @@ def observe( instruction: str | Omit = omit, options: session_observe_params.Options | Omit = omit, stream_response: Literal[False] | Omit = omit, - x_language: Literal["typescript", "python", "playground"] | Omit = omit, - x_sdk_version: str | Omit = omit, x_sent_at: Union[str, datetime] | Omit = omit, x_stream_response: Literal["true", "false"] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. @@ -850,10 +766,6 @@ def observe( stream_response: Whether to stream the response via SSE - x_language: Client SDK language - - x_sdk_version: Version of the Stagehand SDK - x_sent_at: ISO timestamp when request was sent x_stream_response: Whether to stream the response via SSE @@ -877,8 +789,6 @@ def observe( frame_id: str | Omit = omit, instruction: str | Omit = omit, options: session_observe_params.Options | Omit = omit, - x_language: Literal["typescript", "python", "playground"] | Omit = omit, - x_sdk_version: str | Omit = omit, x_sent_at: Union[str, datetime] | Omit = omit, x_stream_response: Literal["true", "false"] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. @@ -901,10 +811,6 @@ def observe( instruction: Natural language instruction for what actions to find - x_language: Client SDK language - - x_sdk_version: Version of the Stagehand SDK - x_sent_at: ISO timestamp when request was sent x_stream_response: Whether to stream the response via SSE @@ -928,8 +834,6 @@ def observe( frame_id: str | Omit = omit, instruction: str | Omit = omit, options: session_observe_params.Options | Omit = omit, - x_language: Literal["typescript", "python", "playground"] | Omit = omit, - x_sdk_version: str | Omit = omit, x_sent_at: Union[str, datetime] | Omit = omit, x_stream_response: Literal["true", "false"] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. @@ -952,10 +856,6 @@ def observe( instruction: Natural language instruction for what actions to find - x_language: Client SDK language - - x_sdk_version: Version of the Stagehand SDK - x_sent_at: ISO timestamp when request was sent x_stream_response: Whether to stream the response via SSE @@ -978,8 +878,6 @@ def observe( instruction: str | Omit = omit, options: session_observe_params.Options | Omit = omit, stream_response: Literal[False] | Literal[True] | Omit = omit, - x_language: Literal["typescript", "python", "playground"] | Omit = omit, - x_sdk_version: str | Omit = omit, x_sent_at: Union[str, datetime] | Omit = omit, x_stream_response: Literal["true", "false"] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. @@ -994,8 +892,6 @@ def observe( extra_headers = { **strip_not_given( { - "x-language": str(x_language) if is_given(x_language) else not_given, - "x-sdk-version": x_sdk_version, "x-sent-at": x_sent_at.isoformat() if is_given(x_sent_at) else not_given, "x-stream-response": str(x_stream_response) if is_given(x_stream_response) else not_given, } @@ -1037,8 +933,6 @@ def start( system_prompt: str | Omit = omit, verbose: Literal[0, 1, 2] | Omit = omit, wait_for_captcha_solves: bool | Omit = omit, - x_language: Literal["typescript", "python", "playground"] | Omit = omit, - x_sdk_version: str | Omit = omit, x_sent_at: Union[str, datetime] | Omit = omit, x_stream_response: Literal["true", "false"] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. @@ -1070,10 +964,6 @@ def start( wait_for_captcha_solves: Wait for captcha solves (deprecated, v2 only) - x_language: Client SDK language - - x_sdk_version: Version of the Stagehand SDK - x_sent_at: ISO timestamp when request was sent x_stream_response: Whether to stream the response via SSE @@ -1089,8 +979,6 @@ def start( extra_headers = { **strip_not_given( { - "x-language": str(x_language) if is_given(x_language) else not_given, - "x-sdk-version": x_sdk_version, "x-sent-at": x_sent_at.isoformat() if is_given(x_sent_at) else not_given, "x-stream-response": str(x_stream_response) if is_given(x_stream_response) else not_given, } @@ -1151,8 +1039,6 @@ async def act( frame_id: str | Omit = omit, options: session_act_params.Options | Omit = omit, stream_response: Literal[False] | Omit = omit, - x_language: Literal["typescript", "python", "playground"] | Omit = omit, - x_sdk_version: str | Omit = omit, x_sent_at: Union[str, datetime] | Omit = omit, x_stream_response: Literal["true", "false"] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. @@ -1175,10 +1061,6 @@ async def act( stream_response: Whether to stream the response via SSE - x_language: Client SDK language - - x_sdk_version: Version of the Stagehand SDK - x_sent_at: ISO timestamp when request was sent x_stream_response: Whether to stream the response via SSE @@ -1202,8 +1084,6 @@ async def act( stream_response: Literal[True], frame_id: str | Omit = omit, options: session_act_params.Options | Omit = omit, - x_language: Literal["typescript", "python", "playground"] | Omit = omit, - x_sdk_version: str | Omit = omit, x_sent_at: Union[str, datetime] | Omit = omit, x_stream_response: Literal["true", "false"] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. @@ -1226,10 +1106,6 @@ async def act( frame_id: Target frame ID for the action - x_language: Client SDK language - - x_sdk_version: Version of the Stagehand SDK - x_sent_at: ISO timestamp when request was sent x_stream_response: Whether to stream the response via SSE @@ -1253,8 +1129,6 @@ async def act( stream_response: bool, frame_id: str | Omit = omit, options: session_act_params.Options | Omit = omit, - x_language: Literal["typescript", "python", "playground"] | Omit = omit, - x_sdk_version: str | Omit = omit, x_sent_at: Union[str, datetime] | Omit = omit, x_stream_response: Literal["true", "false"] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. @@ -1277,10 +1151,6 @@ async def act( frame_id: Target frame ID for the action - x_language: Client SDK language - - x_sdk_version: Version of the Stagehand SDK - x_sent_at: ISO timestamp when request was sent x_stream_response: Whether to stream the response via SSE @@ -1304,8 +1174,6 @@ async def act( frame_id: str | Omit = omit, options: session_act_params.Options | Omit = omit, stream_response: Literal[False] | Literal[True] | Omit = omit, - x_language: Literal["typescript", "python", "playground"] | Omit = omit, - x_sdk_version: str | Omit = omit, x_sent_at: Union[str, datetime] | Omit = omit, x_stream_response: Literal["true", "false"] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. @@ -1320,8 +1188,6 @@ async def act( extra_headers = { **strip_not_given( { - "x-language": str(x_language) if is_given(x_language) else not_given, - "x-sdk-version": x_sdk_version, "x-sent-at": x_sent_at.isoformat() if is_given(x_sent_at) else not_given, "x-stream-response": str(x_stream_response) if is_given(x_stream_response) else not_given, } @@ -1354,8 +1220,6 @@ async def end( id: str, *, _force_body: object | Omit = omit, - x_language: Literal["typescript", "python", "playground"] | Omit = omit, - x_sdk_version: str | Omit = omit, x_sent_at: Union[str, datetime] | Omit = omit, x_stream_response: Literal["true", "false"] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. @@ -1371,10 +1235,6 @@ async def end( Args: id: Unique session identifier - x_language: Client SDK language - - x_sdk_version: Version of the Stagehand SDK - x_sent_at: ISO timestamp when request was sent x_stream_response: Whether to stream the response via SSE @@ -1392,8 +1252,6 @@ async def end( extra_headers = { **strip_not_given( { - "x-language": str(x_language) if is_given(x_language) else not_given, - "x-sdk-version": x_sdk_version, "x-sent-at": x_sent_at.isoformat() if is_given(x_sent_at) else not_given, "x-stream-response": str(x_stream_response) if is_given(x_stream_response) else not_given, } @@ -1418,8 +1276,6 @@ async def execute( execute_options: session_execute_params.ExecuteOptions, frame_id: str | Omit = omit, stream_response: Literal[False] | Omit = omit, - x_language: Literal["typescript", "python", "playground"] | Omit = omit, - x_sdk_version: str | Omit = omit, x_sent_at: Union[str, datetime] | Omit = omit, x_stream_response: Literal["true", "false"] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. @@ -1439,10 +1295,6 @@ async def execute( stream_response: Whether to stream the response via SSE - x_language: Client SDK language - - x_sdk_version: Version of the Stagehand SDK - x_sent_at: ISO timestamp when request was sent x_stream_response: Whether to stream the response via SSE @@ -1466,8 +1318,6 @@ async def execute( execute_options: session_execute_params.ExecuteOptions, stream_response: Literal[True], frame_id: str | Omit = omit, - x_language: Literal["typescript", "python", "playground"] | Omit = omit, - x_sdk_version: str | Omit = omit, x_sent_at: Union[str, datetime] | Omit = omit, x_stream_response: Literal["true", "false"] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. @@ -1487,10 +1337,6 @@ async def execute( frame_id: Target frame ID for the agent - x_language: Client SDK language - - x_sdk_version: Version of the Stagehand SDK - x_sent_at: ISO timestamp when request was sent x_stream_response: Whether to stream the response via SSE @@ -1514,8 +1360,6 @@ async def execute( execute_options: session_execute_params.ExecuteOptions, stream_response: bool, frame_id: str | Omit = omit, - x_language: Literal["typescript", "python", "playground"] | Omit = omit, - x_sdk_version: str | Omit = omit, x_sent_at: Union[str, datetime] | Omit = omit, x_stream_response: Literal["true", "false"] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. @@ -1535,10 +1379,6 @@ async def execute( frame_id: Target frame ID for the agent - x_language: Client SDK language - - x_sdk_version: Version of the Stagehand SDK - x_sent_at: ISO timestamp when request was sent x_stream_response: Whether to stream the response via SSE @@ -1562,8 +1402,6 @@ async def execute( execute_options: session_execute_params.ExecuteOptions, frame_id: str | Omit = omit, stream_response: Literal[False] | Literal[True] | Omit = omit, - x_language: Literal["typescript", "python", "playground"] | Omit = omit, - x_sdk_version: str | Omit = omit, x_sent_at: Union[str, datetime] | Omit = omit, x_stream_response: Literal["true", "false"] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. @@ -1578,8 +1416,6 @@ async def execute( extra_headers = { **strip_not_given( { - "x-language": str(x_language) if is_given(x_language) else not_given, - "x-sdk-version": x_sdk_version, "x-sent-at": x_sent_at.isoformat() if is_given(x_sent_at) else not_given, "x-stream-response": str(x_stream_response) if is_given(x_stream_response) else not_given, } @@ -1617,8 +1453,6 @@ async def extract( options: session_extract_params.Options | Omit = omit, schema: Dict[str, object] | Omit = omit, stream_response: Literal[False] | Omit = omit, - x_language: Literal["typescript", "python", "playground"] | Omit = omit, - x_sdk_version: str | Omit = omit, x_sent_at: Union[str, datetime] | Omit = omit, x_stream_response: Literal["true", "false"] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. @@ -1642,10 +1476,6 @@ async def extract( stream_response: Whether to stream the response via SSE - x_language: Client SDK language - - x_sdk_version: Version of the Stagehand SDK - x_sent_at: ISO timestamp when request was sent x_stream_response: Whether to stream the response via SSE @@ -1670,8 +1500,6 @@ async def extract( instruction: str | Omit = omit, options: session_extract_params.Options | Omit = omit, schema: Dict[str, object] | Omit = omit, - x_language: Literal["typescript", "python", "playground"] | Omit = omit, - x_sdk_version: str | Omit = omit, x_sent_at: Union[str, datetime] | Omit = omit, x_stream_response: Literal["true", "false"] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. @@ -1695,10 +1523,6 @@ async def extract( schema: JSON Schema defining the structure of data to extract - x_language: Client SDK language - - x_sdk_version: Version of the Stagehand SDK - x_sent_at: ISO timestamp when request was sent x_stream_response: Whether to stream the response via SSE @@ -1723,8 +1547,6 @@ async def extract( instruction: str | Omit = omit, options: session_extract_params.Options | Omit = omit, schema: Dict[str, object] | Omit = omit, - x_language: Literal["typescript", "python", "playground"] | Omit = omit, - x_sdk_version: str | Omit = omit, x_sent_at: Union[str, datetime] | Omit = omit, x_stream_response: Literal["true", "false"] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. @@ -1748,10 +1570,6 @@ async def extract( schema: JSON Schema defining the structure of data to extract - x_language: Client SDK language - - x_sdk_version: Version of the Stagehand SDK - x_sent_at: ISO timestamp when request was sent x_stream_response: Whether to stream the response via SSE @@ -1775,8 +1593,6 @@ async def extract( options: session_extract_params.Options | Omit = omit, schema: Dict[str, object] | Omit = omit, stream_response: Literal[False] | Literal[True] | Omit = omit, - x_language: Literal["typescript", "python", "playground"] | Omit = omit, - x_sdk_version: str | Omit = omit, x_sent_at: Union[str, datetime] | Omit = omit, x_stream_response: Literal["true", "false"] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. @@ -1791,8 +1607,6 @@ async def extract( extra_headers = { **strip_not_given( { - "x-language": str(x_language) if is_given(x_language) else not_given, - "x-sdk-version": x_sdk_version, "x-sent-at": x_sent_at.isoformat() if is_given(x_sent_at) else not_given, "x-stream-response": str(x_stream_response) if is_given(x_stream_response) else not_given, } @@ -1829,8 +1643,6 @@ async def navigate( frame_id: str | Omit = omit, options: session_navigate_params.Options | Omit = omit, stream_response: bool | Omit = omit, - x_language: Literal["typescript", "python", "playground"] | Omit = omit, - x_sdk_version: str | Omit = omit, x_sent_at: Union[str, datetime] | Omit = omit, x_stream_response: Literal["true", "false"] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. @@ -1852,10 +1664,6 @@ async def navigate( stream_response: Whether to stream the response via SSE - x_language: Client SDK language - - x_sdk_version: Version of the Stagehand SDK - x_sent_at: ISO timestamp when request was sent x_stream_response: Whether to stream the response via SSE @@ -1873,8 +1681,6 @@ async def navigate( extra_headers = { **strip_not_given( { - "x-language": str(x_language) if is_given(x_language) else not_given, - "x-sdk-version": x_sdk_version, "x-sent-at": x_sent_at.isoformat() if is_given(x_sent_at) else not_given, "x-stream-response": str(x_stream_response) if is_given(x_stream_response) else not_given, } @@ -1907,8 +1713,6 @@ async def observe( instruction: str | Omit = omit, options: session_observe_params.Options | Omit = omit, stream_response: Literal[False] | Omit = omit, - x_language: Literal["typescript", "python", "playground"] | Omit = omit, - x_sdk_version: str | Omit = omit, x_sent_at: Union[str, datetime] | Omit = omit, x_stream_response: Literal["true", "false"] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. @@ -1931,10 +1735,6 @@ async def observe( stream_response: Whether to stream the response via SSE - x_language: Client SDK language - - x_sdk_version: Version of the Stagehand SDK - x_sent_at: ISO timestamp when request was sent x_stream_response: Whether to stream the response via SSE @@ -1958,8 +1758,6 @@ async def observe( frame_id: str | Omit = omit, instruction: str | Omit = omit, options: session_observe_params.Options | Omit = omit, - x_language: Literal["typescript", "python", "playground"] | Omit = omit, - x_sdk_version: str | Omit = omit, x_sent_at: Union[str, datetime] | Omit = omit, x_stream_response: Literal["true", "false"] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. @@ -1982,10 +1780,6 @@ async def observe( instruction: Natural language instruction for what actions to find - x_language: Client SDK language - - x_sdk_version: Version of the Stagehand SDK - x_sent_at: ISO timestamp when request was sent x_stream_response: Whether to stream the response via SSE @@ -2009,8 +1803,6 @@ async def observe( frame_id: str | Omit = omit, instruction: str | Omit = omit, options: session_observe_params.Options | Omit = omit, - x_language: Literal["typescript", "python", "playground"] | Omit = omit, - x_sdk_version: str | Omit = omit, x_sent_at: Union[str, datetime] | Omit = omit, x_stream_response: Literal["true", "false"] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. @@ -2033,10 +1825,6 @@ async def observe( instruction: Natural language instruction for what actions to find - x_language: Client SDK language - - x_sdk_version: Version of the Stagehand SDK - x_sent_at: ISO timestamp when request was sent x_stream_response: Whether to stream the response via SSE @@ -2059,8 +1847,6 @@ async def observe( instruction: str | Omit = omit, options: session_observe_params.Options | Omit = omit, stream_response: Literal[False] | Literal[True] | Omit = omit, - x_language: Literal["typescript", "python", "playground"] | Omit = omit, - x_sdk_version: str | Omit = omit, x_sent_at: Union[str, datetime] | Omit = omit, x_stream_response: Literal["true", "false"] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. @@ -2075,8 +1861,6 @@ async def observe( extra_headers = { **strip_not_given( { - "x-language": str(x_language) if is_given(x_language) else not_given, - "x-sdk-version": x_sdk_version, "x-sent-at": x_sent_at.isoformat() if is_given(x_sent_at) else not_given, "x-stream-response": str(x_stream_response) if is_given(x_stream_response) else not_given, } @@ -2118,8 +1902,6 @@ async def start( system_prompt: str | Omit = omit, verbose: Literal[0, 1, 2] | Omit = omit, wait_for_captcha_solves: bool | Omit = omit, - x_language: Literal["typescript", "python", "playground"] | Omit = omit, - x_sdk_version: str | Omit = omit, x_sent_at: Union[str, datetime] | Omit = omit, x_stream_response: Literal["true", "false"] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. @@ -2151,10 +1933,6 @@ async def start( wait_for_captcha_solves: Wait for captcha solves (deprecated, v2 only) - x_language: Client SDK language - - x_sdk_version: Version of the Stagehand SDK - x_sent_at: ISO timestamp when request was sent x_stream_response: Whether to stream the response via SSE @@ -2170,8 +1948,6 @@ async def start( extra_headers = { **strip_not_given( { - "x-language": str(x_language) if is_given(x_language) else not_given, - "x-sdk-version": x_sdk_version, "x-sent-at": x_sent_at.isoformat() if is_given(x_sent_at) else not_given, "x-stream-response": str(x_stream_response) if is_given(x_stream_response) else not_given, } diff --git a/src/stagehand/types/session_act_params.py b/src/stagehand/types/session_act_params.py index 153ac6c8..605019b8 100644 --- a/src/stagehand/types/session_act_params.py +++ b/src/stagehand/types/session_act_params.py @@ -22,12 +22,6 @@ class SessionActParamsBase(TypedDict, total=False): options: Options - x_language: Annotated[Literal["typescript", "python", "playground"], PropertyInfo(alias="x-language")] - """Client SDK language""" - - x_sdk_version: Annotated[str, PropertyInfo(alias="x-sdk-version")] - """Version of the Stagehand SDK""" - x_sent_at: Annotated[Union[str, datetime], PropertyInfo(alias="x-sent-at", format="iso8601")] """ISO timestamp when request was sent""" diff --git a/src/stagehand/types/session_end_params.py b/src/stagehand/types/session_end_params.py index defdf60a..1998cd27 100644 --- a/src/stagehand/types/session_end_params.py +++ b/src/stagehand/types/session_end_params.py @@ -14,12 +14,6 @@ class SessionEndParams(TypedDict, total=False): _force_body: Annotated[object, PropertyInfo(alias="_forceBody")] - x_language: Annotated[Literal["typescript", "python", "playground"], PropertyInfo(alias="x-language")] - """Client SDK language""" - - x_sdk_version: Annotated[str, PropertyInfo(alias="x-sdk-version")] - """Version of the Stagehand SDK""" - x_sent_at: Annotated[Union[str, datetime], PropertyInfo(alias="x-sent-at", format="iso8601")] """ISO timestamp when request was sent""" diff --git a/src/stagehand/types/session_execute_params.py b/src/stagehand/types/session_execute_params.py index 794181d7..eb8678ff 100644 --- a/src/stagehand/types/session_execute_params.py +++ b/src/stagehand/types/session_execute_params.py @@ -26,12 +26,6 @@ class SessionExecuteParamsBase(TypedDict, total=False): frame_id: Annotated[str, PropertyInfo(alias="frameId")] """Target frame ID for the agent""" - x_language: Annotated[Literal["typescript", "python", "playground"], PropertyInfo(alias="x-language")] - """Client SDK language""" - - x_sdk_version: Annotated[str, PropertyInfo(alias="x-sdk-version")] - """Version of the Stagehand SDK""" - x_sent_at: Annotated[Union[str, datetime], PropertyInfo(alias="x-sent-at", format="iso8601")] """ISO timestamp when request was sent""" diff --git a/src/stagehand/types/session_extract_params.py b/src/stagehand/types/session_extract_params.py index c18800f8..c763cbb5 100644 --- a/src/stagehand/types/session_extract_params.py +++ b/src/stagehand/types/session_extract_params.py @@ -24,12 +24,6 @@ class SessionExtractParamsBase(TypedDict, total=False): schema: Dict[str, object] """JSON Schema defining the structure of data to extract""" - x_language: Annotated[Literal["typescript", "python", "playground"], PropertyInfo(alias="x-language")] - """Client SDK language""" - - x_sdk_version: Annotated[str, PropertyInfo(alias="x-sdk-version")] - """Version of the Stagehand SDK""" - x_sent_at: Annotated[Union[str, datetime], PropertyInfo(alias="x-sent-at", format="iso8601")] """ISO timestamp when request was sent""" diff --git a/src/stagehand/types/session_navigate_params.py b/src/stagehand/types/session_navigate_params.py index 59a86eae..39bb2b2e 100644 --- a/src/stagehand/types/session_navigate_params.py +++ b/src/stagehand/types/session_navigate_params.py @@ -23,12 +23,6 @@ class SessionNavigateParams(TypedDict, total=False): stream_response: Annotated[bool, PropertyInfo(alias="streamResponse")] """Whether to stream the response via SSE""" - x_language: Annotated[Literal["typescript", "python", "playground"], PropertyInfo(alias="x-language")] - """Client SDK language""" - - x_sdk_version: Annotated[str, PropertyInfo(alias="x-sdk-version")] - """Version of the Stagehand SDK""" - x_sent_at: Annotated[Union[str, datetime], PropertyInfo(alias="x-sent-at", format="iso8601")] """ISO timestamp when request was sent""" diff --git a/src/stagehand/types/session_observe_params.py b/src/stagehand/types/session_observe_params.py index 9d18b185..7cad4c85 100644 --- a/src/stagehand/types/session_observe_params.py +++ b/src/stagehand/types/session_observe_params.py @@ -21,12 +21,6 @@ class SessionObserveParamsBase(TypedDict, total=False): options: Options - x_language: Annotated[Literal["typescript", "python", "playground"], PropertyInfo(alias="x-language")] - """Client SDK language""" - - x_sdk_version: Annotated[str, PropertyInfo(alias="x-sdk-version")] - """Version of the Stagehand SDK""" - x_sent_at: Annotated[Union[str, datetime], PropertyInfo(alias="x-sent-at", format="iso8601")] """ISO timestamp when request was sent""" diff --git a/src/stagehand/types/session_start_params.py b/src/stagehand/types/session_start_params.py index 479d1fef..03735673 100644 --- a/src/stagehand/types/session_start_params.py +++ b/src/stagehand/types/session_start_params.py @@ -61,12 +61,6 @@ class SessionStartParams(TypedDict, total=False): wait_for_captcha_solves: Annotated[bool, PropertyInfo(alias="waitForCaptchaSolves")] """Wait for captcha solves (deprecated, v2 only)""" - x_language: Annotated[Literal["typescript", "python", "playground"], PropertyInfo(alias="x-language")] - """Client SDK language""" - - x_sdk_version: Annotated[str, PropertyInfo(alias="x-sdk-version")] - """Version of the Stagehand SDK""" - x_sent_at: Annotated[Union[str, datetime], PropertyInfo(alias="x-sent-at", format="iso8601")] """ISO timestamp when request was sent""" diff --git a/tests/api_resources/test_sessions.py b/tests/api_resources/test_sessions.py index f4d73556..8719e4c5 100644 --- a/tests/api_resources/test_sessions.py +++ b/tests/api_resources/test_sessions.py @@ -48,8 +48,6 @@ def test_method_act_with_all_params_overload_1(self, client: Stagehand) -> None: "variables": {"username": "john_doe"}, }, stream_response=False, - x_language="typescript", - x_sdk_version="3.0.6", x_sent_at=parse_datetime("2025-01-15T10:30:00Z"), x_stream_response="true", ) @@ -115,8 +113,6 @@ def test_method_act_with_all_params_overload_2(self, client: Stagehand) -> None: "timeout": 30000, "variables": {"username": "john_doe"}, }, - x_language="typescript", - x_sdk_version="3.0.6", x_sent_at=parse_datetime("2025-01-15T10:30:00Z"), x_stream_response="true", ) @@ -175,8 +171,6 @@ def test_method_end_with_all_params(self, client: Stagehand) -> None: session = client.sessions.end( id="c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123", _force_body={}, - x_language="typescript", - x_sdk_version="3.0.6", x_sent_at=parse_datetime("2025-01-15T10:30:00Z"), x_stream_response="true", ) @@ -246,8 +240,6 @@ def test_method_execute_with_all_params_overload_1(self, client: Stagehand) -> N }, frame_id="frameId", stream_response=False, - x_language="typescript", - x_sdk_version="3.0.6", x_sent_at=parse_datetime("2025-01-15T10:30:00Z"), x_stream_response="true", ) @@ -330,8 +322,6 @@ def test_method_execute_with_all_params_overload_2(self, client: Stagehand) -> N }, stream_response=True, frame_id="frameId", - x_language="typescript", - x_sdk_version="3.0.6", x_sent_at=parse_datetime("2025-01-15T10:30:00Z"), x_stream_response="true", ) @@ -407,8 +397,6 @@ def test_method_extract_with_all_params_overload_1(self, client: Stagehand) -> N }, schema={"foo": "bar"}, stream_response=False, - x_language="typescript", - x_sdk_version="3.0.6", x_sent_at=parse_datetime("2025-01-15T10:30:00Z"), x_stream_response="true", ) @@ -471,8 +459,6 @@ def test_method_extract_with_all_params_overload_2(self, client: Stagehand) -> N "timeout": 30000, }, schema={"foo": "bar"}, - x_language="typescript", - x_sdk_version="3.0.6", x_sent_at=parse_datetime("2025-01-15T10:30:00Z"), x_stream_response="true", ) @@ -536,8 +522,6 @@ def test_method_navigate_with_all_params(self, client: Stagehand) -> None: "wait_until": "networkidle", }, stream_response=True, - x_language="typescript", - x_sdk_version="3.0.6", x_sent_at=parse_datetime("2025-01-15T10:30:00Z"), x_stream_response="true", ) @@ -601,8 +585,6 @@ def test_method_observe_with_all_params_overload_1(self, client: Stagehand) -> N "timeout": 30000, }, stream_response=False, - x_language="typescript", - x_sdk_version="3.0.6", x_sent_at=parse_datetime("2025-01-15T10:30:00Z"), x_stream_response="true", ) @@ -664,8 +646,6 @@ def test_method_observe_with_all_params_overload_2(self, client: Stagehand) -> N "selector": "nav", "timeout": 30000, }, - x_language="typescript", - x_sdk_version="3.0.6", x_sent_at=parse_datetime("2025-01-15T10:30:00Z"), x_stream_response="true", ) @@ -798,8 +778,6 @@ def test_method_start_with_all_params(self, client: Stagehand) -> None: system_prompt="systemPrompt", verbose=1, wait_for_captcha_solves=True, - x_language="typescript", - x_sdk_version="3.0.6", x_sent_at=parse_datetime("2025-01-15T10:30:00Z"), x_stream_response="true", ) @@ -859,8 +837,6 @@ async def test_method_act_with_all_params_overload_1(self, async_client: AsyncSt "variables": {"username": "john_doe"}, }, stream_response=False, - x_language="typescript", - x_sdk_version="3.0.6", x_sent_at=parse_datetime("2025-01-15T10:30:00Z"), x_stream_response="true", ) @@ -926,8 +902,6 @@ async def test_method_act_with_all_params_overload_2(self, async_client: AsyncSt "timeout": 30000, "variables": {"username": "john_doe"}, }, - x_language="typescript", - x_sdk_version="3.0.6", x_sent_at=parse_datetime("2025-01-15T10:30:00Z"), x_stream_response="true", ) @@ -986,8 +960,6 @@ async def test_method_end_with_all_params(self, async_client: AsyncStagehand) -> session = await async_client.sessions.end( id="c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123", _force_body={}, - x_language="typescript", - x_sdk_version="3.0.6", x_sent_at=parse_datetime("2025-01-15T10:30:00Z"), x_stream_response="true", ) @@ -1057,8 +1029,6 @@ async def test_method_execute_with_all_params_overload_1(self, async_client: Asy }, frame_id="frameId", stream_response=False, - x_language="typescript", - x_sdk_version="3.0.6", x_sent_at=parse_datetime("2025-01-15T10:30:00Z"), x_stream_response="true", ) @@ -1141,8 +1111,6 @@ async def test_method_execute_with_all_params_overload_2(self, async_client: Asy }, stream_response=True, frame_id="frameId", - x_language="typescript", - x_sdk_version="3.0.6", x_sent_at=parse_datetime("2025-01-15T10:30:00Z"), x_stream_response="true", ) @@ -1218,8 +1186,6 @@ async def test_method_extract_with_all_params_overload_1(self, async_client: Asy }, schema={"foo": "bar"}, stream_response=False, - x_language="typescript", - x_sdk_version="3.0.6", x_sent_at=parse_datetime("2025-01-15T10:30:00Z"), x_stream_response="true", ) @@ -1282,8 +1248,6 @@ async def test_method_extract_with_all_params_overload_2(self, async_client: Asy "timeout": 30000, }, schema={"foo": "bar"}, - x_language="typescript", - x_sdk_version="3.0.6", x_sent_at=parse_datetime("2025-01-15T10:30:00Z"), x_stream_response="true", ) @@ -1347,8 +1311,6 @@ async def test_method_navigate_with_all_params(self, async_client: AsyncStagehan "wait_until": "networkidle", }, stream_response=True, - x_language="typescript", - x_sdk_version="3.0.6", x_sent_at=parse_datetime("2025-01-15T10:30:00Z"), x_stream_response="true", ) @@ -1412,8 +1374,6 @@ async def test_method_observe_with_all_params_overload_1(self, async_client: Asy "timeout": 30000, }, stream_response=False, - x_language="typescript", - x_sdk_version="3.0.6", x_sent_at=parse_datetime("2025-01-15T10:30:00Z"), x_stream_response="true", ) @@ -1475,8 +1435,6 @@ async def test_method_observe_with_all_params_overload_2(self, async_client: Asy "selector": "nav", "timeout": 30000, }, - x_language="typescript", - x_sdk_version="3.0.6", x_sent_at=parse_datetime("2025-01-15T10:30:00Z"), x_stream_response="true", ) @@ -1609,8 +1567,6 @@ async def test_method_start_with_all_params(self, async_client: AsyncStagehand) system_prompt="systemPrompt", verbose=1, wait_for_captcha_solves=True, - x_language="typescript", - x_sdk_version="3.0.6", x_sent_at=parse_datetime("2025-01-15T10:30:00Z"), x_stream_response="true", ) From 2a87525125540a8bb92f1b79e929ff32d7b5ff5b Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 12 Jan 2026 20:09:20 +0000 Subject: [PATCH 60/88] feat: Using provider/model syntax in modelName examples within openapi spec --- .stats.yml | 6 +++--- src/stagehand/types/model_config_param.py | 2 +- tests/api_resources/test_sessions.py | 16 ++++++++-------- tests/test_client.py | 20 ++++++++++---------- 4 files changed, 22 insertions(+), 22 deletions(-) diff --git a/.stats.yml b/.stats.yml index f53d855f..93fe5d17 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 7 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fstagehand-1705ff86e7ec80d6be2ddbb0e3cbee821f3e95d68fa6a48c790f586e3470e678.yml -openapi_spec_hash: cf0d4dad078a7f7c1256b437e349b911 -config_hash: 3c21550e2c94cad4339d3093d794beb0 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fstagehand-119383e808f394a7676e901bac8b97b6d7402d187d03452fd8d62b31d4085580.yml +openapi_spec_hash: 8a8d7be19d95f849098690863fe9a71a +config_hash: 1f709f8775e13029dc60064ef3a94355 diff --git a/src/stagehand/types/model_config_param.py b/src/stagehand/types/model_config_param.py index 8d3df0ef..f633db12 100644 --- a/src/stagehand/types/model_config_param.py +++ b/src/stagehand/types/model_config_param.py @@ -12,7 +12,7 @@ class ModelConfigObject(TypedDict, total=False): model_name: Required[Annotated[str, PropertyInfo(alias="modelName")]] - """Model name string without prefix (e.g., 'gpt-5-nano', 'claude-4.5-opus')""" + """Model name string (e.g., 'openai/gpt-5-nano', 'anthropic/claude-4.5-opus')""" api_key: Annotated[str, PropertyInfo(alias="apiKey")] """API key for the model provider""" diff --git a/tests/api_resources/test_sessions.py b/tests/api_resources/test_sessions.py index 8719e4c5..473a917d 100644 --- a/tests/api_resources/test_sessions.py +++ b/tests/api_resources/test_sessions.py @@ -691,7 +691,7 @@ def test_path_params_observe_overload_2(self, client: Stagehand) -> None: @parametrize def test_method_start(self, client: Stagehand) -> None: session = client.sessions.start( - model_name="gpt-4o", + model_name="openai/gpt-4o", ) assert_matches_type(SessionStartResponse, session, path=["response"]) @@ -699,7 +699,7 @@ def test_method_start(self, client: Stagehand) -> None: @parametrize def test_method_start_with_all_params(self, client: Stagehand) -> None: session = client.sessions.start( - model_name="gpt-4o", + model_name="openai/gpt-4o", act_timeout_ms=0, browser={ "cdp_url": "ws://localhost:9222", @@ -787,7 +787,7 @@ def test_method_start_with_all_params(self, client: Stagehand) -> None: @parametrize def test_raw_response_start(self, client: Stagehand) -> None: response = client.sessions.with_raw_response.start( - model_name="gpt-4o", + model_name="openai/gpt-4o", ) assert response.is_closed is True @@ -799,7 +799,7 @@ def test_raw_response_start(self, client: Stagehand) -> None: @parametrize def test_streaming_response_start(self, client: Stagehand) -> None: with client.sessions.with_streaming_response.start( - model_name="gpt-4o", + model_name="openai/gpt-4o", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -1480,7 +1480,7 @@ async def test_path_params_observe_overload_2(self, async_client: AsyncStagehand @parametrize async def test_method_start(self, async_client: AsyncStagehand) -> None: session = await async_client.sessions.start( - model_name="gpt-4o", + model_name="openai/gpt-4o", ) assert_matches_type(SessionStartResponse, session, path=["response"]) @@ -1488,7 +1488,7 @@ async def test_method_start(self, async_client: AsyncStagehand) -> None: @parametrize async def test_method_start_with_all_params(self, async_client: AsyncStagehand) -> None: session = await async_client.sessions.start( - model_name="gpt-4o", + model_name="openai/gpt-4o", act_timeout_ms=0, browser={ "cdp_url": "ws://localhost:9222", @@ -1576,7 +1576,7 @@ async def test_method_start_with_all_params(self, async_client: AsyncStagehand) @parametrize async def test_raw_response_start(self, async_client: AsyncStagehand) -> None: response = await async_client.sessions.with_raw_response.start( - model_name="gpt-4o", + model_name="openai/gpt-4o", ) assert response.is_closed is True @@ -1588,7 +1588,7 @@ async def test_raw_response_start(self, async_client: AsyncStagehand) -> None: @parametrize async def test_streaming_response_start(self, async_client: AsyncStagehand) -> None: async with async_client.sessions.with_streaming_response.start( - model_name="gpt-4o", + model_name="openai/gpt-4o", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" diff --git a/tests/test_client.py b/tests/test_client.py index b1d79c7a..b04449bf 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -891,7 +891,7 @@ def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter, clien respx_mock.post("/v1/sessions/start").mock(side_effect=httpx.TimeoutException("Test timeout error")) with pytest.raises(APITimeoutError): - client.sessions.with_streaming_response.start(model_name="gpt-4o").__enter__() + client.sessions.with_streaming_response.start(model_name="openai/gpt-4o").__enter__() assert _get_open_connections(client) == 0 @@ -901,7 +901,7 @@ def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter, client respx_mock.post("/v1/sessions/start").mock(return_value=httpx.Response(500)) with pytest.raises(APIStatusError): - client.sessions.with_streaming_response.start(model_name="gpt-4o").__enter__() + client.sessions.with_streaming_response.start(model_name="openai/gpt-4o").__enter__() assert _get_open_connections(client) == 0 @pytest.mark.parametrize("failures_before_success", [0, 2, 4]) @@ -930,7 +930,7 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: respx_mock.post("/v1/sessions/start").mock(side_effect=retry_handler) - response = client.sessions.with_raw_response.start(model_name="gpt-4o") + response = client.sessions.with_raw_response.start(model_name="openai/gpt-4o") assert response.retries_taken == failures_before_success assert int(response.http_request.headers.get("x-stainless-retry-count")) == failures_before_success @@ -955,7 +955,7 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: respx_mock.post("/v1/sessions/start").mock(side_effect=retry_handler) response = client.sessions.with_raw_response.start( - model_name="gpt-4o", extra_headers={"x-stainless-retry-count": Omit()} + model_name="openai/gpt-4o", extra_headers={"x-stainless-retry-count": Omit()} ) assert len(response.http_request.headers.get_list("x-stainless-retry-count")) == 0 @@ -980,7 +980,7 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: respx_mock.post("/v1/sessions/start").mock(side_effect=retry_handler) response = client.sessions.with_raw_response.start( - model_name="gpt-4o", extra_headers={"x-stainless-retry-count": "42"} + model_name="openai/gpt-4o", extra_headers={"x-stainless-retry-count": "42"} ) assert response.http_request.headers.get("x-stainless-retry-count") == "42" @@ -1870,7 +1870,7 @@ async def test_retrying_timeout_errors_doesnt_leak( respx_mock.post("/v1/sessions/start").mock(side_effect=httpx.TimeoutException("Test timeout error")) with pytest.raises(APITimeoutError): - await async_client.sessions.with_streaming_response.start(model_name="gpt-4o").__aenter__() + await async_client.sessions.with_streaming_response.start(model_name="openai/gpt-4o").__aenter__() assert _get_open_connections(async_client) == 0 @@ -1882,7 +1882,7 @@ async def test_retrying_status_errors_doesnt_leak( respx_mock.post("/v1/sessions/start").mock(return_value=httpx.Response(500)) with pytest.raises(APIStatusError): - await async_client.sessions.with_streaming_response.start(model_name="gpt-4o").__aenter__() + await async_client.sessions.with_streaming_response.start(model_name="openai/gpt-4o").__aenter__() assert _get_open_connections(async_client) == 0 @pytest.mark.parametrize("failures_before_success", [0, 2, 4]) @@ -1911,7 +1911,7 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: respx_mock.post("/v1/sessions/start").mock(side_effect=retry_handler) - response = await client.sessions.with_raw_response.start(model_name="gpt-4o") + response = await client.sessions.with_raw_response.start(model_name="openai/gpt-4o") assert response.retries_taken == failures_before_success assert int(response.http_request.headers.get("x-stainless-retry-count")) == failures_before_success @@ -1936,7 +1936,7 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: respx_mock.post("/v1/sessions/start").mock(side_effect=retry_handler) response = await client.sessions.with_raw_response.start( - model_name="gpt-4o", extra_headers={"x-stainless-retry-count": Omit()} + model_name="openai/gpt-4o", extra_headers={"x-stainless-retry-count": Omit()} ) assert len(response.http_request.headers.get_list("x-stainless-retry-count")) == 0 @@ -1961,7 +1961,7 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: respx_mock.post("/v1/sessions/start").mock(side_effect=retry_handler) response = await client.sessions.with_raw_response.start( - model_name="gpt-4o", extra_headers={"x-stainless-retry-count": "42"} + model_name="openai/gpt-4o", extra_headers={"x-stainless-retry-count": "42"} ) assert response.http_request.headers.get("x-stainless-retry-count") == "42" From 8e9be800a4628995019e473aab3c99e0a6a77ddc Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 12 Jan 2026 21:22:26 +0000 Subject: [PATCH 61/88] chore(internal): version bump --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/stagehand/_version.py | 2 +- uv.lock | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 9aa540f7..6b7b74c5 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.2.4" + ".": "0.3.0" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 5be108ae..2a2d179f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "stagehand-alpha" -version = "0.2.4" +version = "0.3.0" description = "The official Python library for the stagehand API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/stagehand/_version.py b/src/stagehand/_version.py index f26dd6ac..a162c62a 100644 --- a/src/stagehand/_version.py +++ b/src/stagehand/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "stagehand" -__version__ = "0.2.4" # x-release-please-version +__version__ = "0.3.0" # x-release-please-version diff --git a/uv.lock b/uv.lock index 512dee99..161e0116 100644 --- a/uv.lock +++ b/uv.lock @@ -1339,7 +1339,7 @@ wheels = [ [[package]] name = "stagehand-alpha" -version = "0.2.4" +version = "0.3.0" source = { editable = "." } dependencies = [ { name = "anyio" }, From 53933039a517ba389678e51871ed91a42a8e8376 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 13 Jan 2026 00:47:44 +0000 Subject: [PATCH 62/88] codegen metadata --- .stats.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index 93fe5d17..31ed2c40 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 7 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fstagehand-119383e808f394a7676e901bac8b97b6d7402d187d03452fd8d62b31d4085580.yml -openapi_spec_hash: 8a8d7be19d95f849098690863fe9a71a +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fstagehand-419940ce988c43313660d30a5bb5b5c2d89b3b19a0f80fe050331e0f4e8c58d2.yml +openapi_spec_hash: a621ca69697ebba7286cbf9e475c46ad config_hash: 1f709f8775e13029dc60064ef3a94355 From e20807c2ece32ae05e7e334a8f0f411d5ddec6fe Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 13 Jan 2026 00:49:21 +0000 Subject: [PATCH 63/88] chore(internal): version bump --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/stagehand/_version.py | 2 +- uv.lock | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 6b7b74c5..cce92405 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.3.0" + ".": "0.3.1" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 2a2d179f..445a691c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "stagehand-alpha" -version = "0.3.0" +version = "0.3.1" description = "The official Python library for the stagehand API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/stagehand/_version.py b/src/stagehand/_version.py index a162c62a..76d534d7 100644 --- a/src/stagehand/_version.py +++ b/src/stagehand/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "stagehand" -__version__ = "0.3.0" # x-release-please-version +__version__ = "0.3.1" # x-release-please-version diff --git a/uv.lock b/uv.lock index 161e0116..676882f6 100644 --- a/uv.lock +++ b/uv.lock @@ -1339,7 +1339,7 @@ wheels = [ [[package]] name = "stagehand-alpha" -version = "0.3.0" +version = "0.3.1" source = { editable = "." } dependencies = [ { name = "anyio" }, From a39bc4c11b5bf2dc8dfff7c8fb7a61a3ddd6e1ce Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 13 Jan 2026 00:59:30 +0000 Subject: [PATCH 64/88] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index 31ed2c40..a03dd758 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 7 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fstagehand-419940ce988c43313660d30a5bb5b5c2d89b3b19a0f80fe050331e0f4e8c58d2.yml openapi_spec_hash: a621ca69697ebba7286cbf9e475c46ad -config_hash: 1f709f8775e13029dc60064ef3a94355 +config_hash: 74111faa0876db6b053526281c444498 From 49ef2808fd31c6a9e0a1550a8a03ccd8527579da Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 13 Jan 2026 17:19:26 +0000 Subject: [PATCH 65/88] codegen metadata --- .stats.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.stats.yml b/.stats.yml index a03dd758..34673800 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 7 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fstagehand-419940ce988c43313660d30a5bb5b5c2d89b3b19a0f80fe050331e0f4e8c58d2.yml -openapi_spec_hash: a621ca69697ebba7286cbf9e475c46ad -config_hash: 74111faa0876db6b053526281c444498 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fstagehand-8210daebb6c6b76559ae5c0ca4c0a9189f444da8afb5db1a544395a66ae14a2f.yml +openapi_spec_hash: 2255c1c27382b4227754d64d85258a82 +config_hash: 46336540a7b06c89308171858ad8238c From b2d6b31ffd99f38d11bac83ddb84975b7b6c48eb Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 13 Jan 2026 17:54:31 +0000 Subject: [PATCH 66/88] codegen metadata --- .stats.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index 34673800..cb872bbf 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 7 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fstagehand-8210daebb6c6b76559ae5c0ca4c0a9189f444da8afb5db1a544395a66ae14a2f.yml -openapi_spec_hash: 2255c1c27382b4227754d64d85258a82 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fstagehand-991d1530002115ecec027f98cad357d39ca1ece6784f62d48e6740b8830e1104.yml +openapi_spec_hash: 303329893ced56b2c129fb9fd666144e config_hash: 46336540a7b06c89308171858ad8238c From 8d8ca6dbb69d36bc1dc62e83da2d998903f6a9c1 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 13 Jan 2026 18:09:07 +0000 Subject: [PATCH 67/88] chore: update SDK settings --- .stats.yml | 2 +- README.md | 6 +- pyproject.toml | 2 +- requirements-dev.lock | 12 +-- uv.lock | 170 +++++++++++++++++++++--------------------- 5 files changed, 96 insertions(+), 96 deletions(-) diff --git a/.stats.yml b/.stats.yml index cb872bbf..9affb565 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 7 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fstagehand-991d1530002115ecec027f98cad357d39ca1ece6784f62d48e6740b8830e1104.yml openapi_spec_hash: 303329893ced56b2c129fb9fd666144e -config_hash: 46336540a7b06c89308171858ad8238c +config_hash: 6638c79b9a4c739a7f1154eb16321d48 diff --git a/README.md b/README.md index fdecf96a..a0a19e88 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # Stagehand Python API library -[![PyPI version](https://img.shields.io/pypi/v/stagehand-alpha.svg?label=pypi%20(stable))](https://pypi.org/project/stagehand-alpha/) +[![PyPI version](https://img.shields.io/pypi/v/stagehand.svg?label=pypi%20(stable))](https://pypi.org/project/stagehand/) The Stagehand Python library provides convenient access to the Stagehand REST API from any Python 3.9+ application. The library includes type definitions for all request params and response fields, @@ -26,7 +26,7 @@ The REST API documentation can be found on [docs.stagehand.dev](https://docs.sta ```sh # install from PyPI -pip install stagehand-alpha +pip install stagehand ``` ## Usage @@ -100,7 +100,7 @@ You can enable this by installing `aiohttp`: ```sh # install from PyPI -pip install stagehand-alpha[aiohttp] +pip install stagehand[aiohttp] ``` Then you can enable it by instantiating the client with `http_client=DefaultAioHttpClient()`: diff --git a/pyproject.toml b/pyproject.toml index 445a691c..5c7955de 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [project] -name = "stagehand-alpha" +name = "stagehand" version = "0.3.1" description = "The official Python library for the stagehand API" dynamic = ["readme"] diff --git a/requirements-dev.lock b/requirements-dev.lock index 0bb435b6..db709b17 100644 --- a/requirements-dev.lock +++ b/requirements-dev.lock @@ -6,7 +6,7 @@ annotated-types==0.7.0 anyio==4.12.0 # via # httpx - # stagehand-alpha + # stagehand backports-asyncio-runner==1.2.0 ; python_full_version < '3.11' # via pytest-asyncio certifi==2025.11.12 @@ -17,7 +17,7 @@ colorama==0.4.6 ; sys_platform == 'win32' # via pytest dirty-equals==0.11 distro==1.9.0 - # via stagehand-alpha + # via stagehand exceptiongroup==1.3.1 ; python_full_version < '3.11' # via # anyio @@ -31,7 +31,7 @@ httpcore==1.0.9 httpx==0.28.1 # via # respx - # stagehand-alpha + # stagehand idna==3.11 # via # anyio @@ -59,7 +59,7 @@ pathspec==0.12.1 pluggy==1.6.0 # via pytest pydantic==2.12.5 - # via stagehand-alpha + # via stagehand pydantic-core==2.41.5 # via pydantic pygments==2.19.2 @@ -86,7 +86,7 @@ ruff==0.14.7 six==1.17.0 ; python_full_version < '3.10' # via python-dateutil sniffio==1.3.1 - # via stagehand-alpha + # via stagehand time-machine==2.19.0 ; python_full_version < '3.10' time-machine==3.1.0 ; python_full_version >= '3.10' tomli==2.3.0 ; python_full_version < '3.11' @@ -102,7 +102,7 @@ typing-extensions==4.15.0 # pydantic-core # pyright # pytest-asyncio - # stagehand-alpha + # stagehand # typing-inspection typing-inspection==0.4.2 # via pydantic diff --git a/uv.lock b/uv.lock index 676882f6..94d26518 100644 --- a/uv.lock +++ b/uv.lock @@ -2,17 +2,17 @@ version = 1 revision = 3 requires-python = ">=3.9" resolution-markers = [ - "python_full_version >= '3.14' and extra != 'group-15-stagehand-alpha-pydantic-v1' and extra == 'group-15-stagehand-alpha-pydantic-v2'", - "python_full_version >= '3.10' and python_full_version < '3.14' and extra != 'group-15-stagehand-alpha-pydantic-v1' and extra == 'group-15-stagehand-alpha-pydantic-v2'", - "python_full_version < '3.10' and extra != 'group-15-stagehand-alpha-pydantic-v1' and extra == 'group-15-stagehand-alpha-pydantic-v2'", - "python_full_version >= '3.10' and extra == 'group-15-stagehand-alpha-pydantic-v1' and extra != 'group-15-stagehand-alpha-pydantic-v2'", - "python_full_version < '3.10' and extra == 'group-15-stagehand-alpha-pydantic-v1' and extra != 'group-15-stagehand-alpha-pydantic-v2'", - "python_full_version >= '3.10' and extra != 'group-15-stagehand-alpha-pydantic-v1' and extra != 'group-15-stagehand-alpha-pydantic-v2'", - "python_full_version < '3.10' and extra != 'group-15-stagehand-alpha-pydantic-v1' and extra != 'group-15-stagehand-alpha-pydantic-v2'", + "python_full_version >= '3.14' and extra != 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2'", + "python_full_version >= '3.10' and python_full_version < '3.14' and extra != 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2'", + "python_full_version < '3.10' and extra != 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2'", + "python_full_version >= '3.10' and extra == 'group-9-stagehand-pydantic-v1' and extra != 'group-9-stagehand-pydantic-v2'", + "python_full_version < '3.10' and extra == 'group-9-stagehand-pydantic-v1' and extra != 'group-9-stagehand-pydantic-v2'", + "python_full_version >= '3.10' and extra != 'group-9-stagehand-pydantic-v1' and extra != 'group-9-stagehand-pydantic-v2'", + "python_full_version < '3.10' and extra != 'group-9-stagehand-pydantic-v1' and extra != 'group-9-stagehand-pydantic-v2'", ] conflicts = [[ - { package = "stagehand-alpha", group = "pydantic-v1" }, - { package = "stagehand-alpha", group = "pydantic-v2" }, + { package = "stagehand", group = "pydantic-v1" }, + { package = "stagehand", group = "pydantic-v2" }, ]] [[package]] @@ -31,7 +31,7 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiohappyeyeballs" }, { name = "aiosignal" }, - { name = "async-timeout", marker = "python_full_version < '3.11' or (extra == 'group-15-stagehand-alpha-pydantic-v1' and extra == 'group-15-stagehand-alpha-pydantic-v2')" }, + { name = "async-timeout", marker = "python_full_version < '3.11' or (extra == 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2')" }, { name = "attrs" }, { name = "frozenlist" }, { name = "multidict" }, @@ -167,7 +167,7 @@ version = "1.4.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "frozenlist" }, - { name = "typing-extensions", marker = "python_full_version < '3.13' or (extra == 'group-15-stagehand-alpha-pydantic-v1' and extra == 'group-15-stagehand-alpha-pydantic-v2')" }, + { name = "typing-extensions", marker = "python_full_version < '3.13' or (extra == 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/61/62/06741b579156360248d1ec624842ad0edf697050bbaf7c3e46394e106ad1/aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7", size = 25007, upload-time = "2025-07-03T22:54:43.528Z" } wheels = [ @@ -188,9 +188,9 @@ name = "anyio" version = "4.12.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "exceptiongroup", marker = "python_full_version < '3.11' or (extra == 'group-15-stagehand-alpha-pydantic-v1' and extra == 'group-15-stagehand-alpha-pydantic-v2')" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11' or (extra == 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2')" }, { name = "idna" }, - { name = "typing-extensions", marker = "python_full_version < '3.13' or (extra == 'group-15-stagehand-alpha-pydantic-v1' and extra == 'group-15-stagehand-alpha-pydantic-v2')" }, + { name = "typing-extensions", marker = "python_full_version < '3.13' or (extra == 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/16/ce/8a777047513153587e5434fd752e89334ac33e379aa3497db860eeb60377/anyio-4.12.0.tar.gz", hash = "sha256:73c693b567b0c55130c104d0b43a9baf3aa6a31fc6110116509f27bf75e21ec0", size = 228266, upload-time = "2025-11-28T23:37:38.911Z" } wheels = [ @@ -265,7 +265,7 @@ name = "exceptiongroup" version = "1.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions", marker = "python_full_version < '3.13' or (extra == 'group-15-stagehand-alpha-pydantic-v1' and extra == 'group-15-stagehand-alpha-pydantic-v2')" }, + { name = "typing-extensions", marker = "python_full_version < '3.13' or (extra == 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2')" }, ] 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 = [ @@ -506,10 +506,10 @@ name = "iniconfig" version = "2.3.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.14' and extra != 'group-15-stagehand-alpha-pydantic-v1' and extra == 'group-15-stagehand-alpha-pydantic-v2'", - "python_full_version >= '3.10' and python_full_version < '3.14' and extra != 'group-15-stagehand-alpha-pydantic-v1' and extra == 'group-15-stagehand-alpha-pydantic-v2'", - "python_full_version >= '3.10' and extra == 'group-15-stagehand-alpha-pydantic-v1' and extra != 'group-15-stagehand-alpha-pydantic-v2'", - "python_full_version >= '3.10' and extra != 'group-15-stagehand-alpha-pydantic-v1' and extra != 'group-15-stagehand-alpha-pydantic-v2'", + "python_full_version >= '3.14' and extra != 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2'", + "python_full_version >= '3.10' and python_full_version < '3.14' and extra != 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2'", + "python_full_version >= '3.10' and extra == 'group-9-stagehand-pydantic-v1' and extra != 'group-9-stagehand-pydantic-v2'", + "python_full_version >= '3.10' and extra != 'group-9-stagehand-pydantic-v1' and extra != 'group-9-stagehand-pydantic-v2'", ] sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } wheels = [ @@ -524,7 +524,7 @@ resolution-markers = [ "python_full_version < '3.10'", ] dependencies = [ - { name = "mdurl", marker = "python_full_version < '3.10' or (extra == 'group-15-stagehand-alpha-pydantic-v1' and extra == 'group-15-stagehand-alpha-pydantic-v2')" }, + { name = "mdurl", marker = "python_full_version < '3.10' or (extra == 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596, upload-time = "2023-06-03T06:41:14.443Z" } wheels = [ @@ -536,13 +536,13 @@ name = "markdown-it-py" version = "4.0.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.14' and extra != 'group-15-stagehand-alpha-pydantic-v1' and extra == 'group-15-stagehand-alpha-pydantic-v2'", - "python_full_version >= '3.10' and python_full_version < '3.14' and extra != 'group-15-stagehand-alpha-pydantic-v1' and extra == 'group-15-stagehand-alpha-pydantic-v2'", - "python_full_version >= '3.10' and extra == 'group-15-stagehand-alpha-pydantic-v1' and extra != 'group-15-stagehand-alpha-pydantic-v2'", - "python_full_version >= '3.10' and extra != 'group-15-stagehand-alpha-pydantic-v1' and extra != 'group-15-stagehand-alpha-pydantic-v2'", + "python_full_version >= '3.14' and extra != 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2'", + "python_full_version >= '3.10' and python_full_version < '3.14' and extra != 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2'", + "python_full_version >= '3.10' and extra == 'group-9-stagehand-pydantic-v1' and extra != 'group-9-stagehand-pydantic-v2'", + "python_full_version >= '3.10' and extra != 'group-9-stagehand-pydantic-v1' and extra != 'group-9-stagehand-pydantic-v2'", ] dependencies = [ - { name = "mdurl", marker = "python_full_version >= '3.10' or (extra == 'group-15-stagehand-alpha-pydantic-v1' and extra == 'group-15-stagehand-alpha-pydantic-v2')" }, + { name = "mdurl", marker = "python_full_version >= '3.10' or (extra == 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" } wheels = [ @@ -563,7 +563,7 @@ name = "multidict" version = "6.7.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions", marker = "python_full_version < '3.11' or (extra == 'group-15-stagehand-alpha-pydantic-v1' and extra == 'group-15-stagehand-alpha-pydantic-v2')" }, + { name = "typing-extensions", marker = "python_full_version < '3.11' or (extra == 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/80/1e/5492c365f222f907de1039b91f922b93fa4f764c713ee858d235495d8f50/multidict-6.7.0.tar.gz", hash = "sha256:c6e99d9a65ca282e578dfea819cfa9c0a62b2499d8677392e09feaf305e9e6f5", size = 101834, upload-time = "2025-10-06T14:52:30.657Z" } wheels = [ @@ -721,7 +721,7 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mypy-extensions" }, { name = "pathspec" }, - { name = "tomli", marker = "python_full_version < '3.11' or (extra == 'group-15-stagehand-alpha-pydantic-v1' and extra == 'group-15-stagehand-alpha-pydantic-v2')" }, + { name = "tomli", marker = "python_full_version < '3.11' or (extra == 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2')" }, { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/1e/e3/034322d5a779685218ed69286c32faa505247f1f096251ef66c8fd203b08/mypy-1.17.0.tar.gz", hash = "sha256:e5d7ccc08ba089c06e2f5629c660388ef1fee708444f1dee0b9203fa031dee03", size = 3352114, upload-time = "2025-07-14T20:34:30.181Z" } @@ -942,7 +942,7 @@ resolution-markers = [ "python_full_version < '3.10'", ] dependencies = [ - { name = "typing-extensions", marker = "extra == 'group-15-stagehand-alpha-pydantic-v1'" }, + { name = "typing-extensions", marker = "extra == 'group-9-stagehand-pydantic-v1'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/ae/8d/7b346ed940c3e0f9eee7db9be37915a6dac0d9535d736e2ca47a81a066f3/pydantic-1.10.24.tar.gz", hash = "sha256:7e6d1af1bd3d2312079f28c9baf2aafb4a452a06b50717526e5ac562e37baa53", size = 357314, upload-time = "2025-09-25T01:36:33.065Z" } wheels = [ @@ -989,17 +989,17 @@ name = "pydantic" version = "2.12.5" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.14' and extra != 'group-15-stagehand-alpha-pydantic-v1' and extra == 'group-15-stagehand-alpha-pydantic-v2'", - "python_full_version >= '3.10' and python_full_version < '3.14' and extra != 'group-15-stagehand-alpha-pydantic-v1' and extra == 'group-15-stagehand-alpha-pydantic-v2'", - "python_full_version < '3.10' and extra != 'group-15-stagehand-alpha-pydantic-v1' and extra == 'group-15-stagehand-alpha-pydantic-v2'", - "python_full_version >= '3.10' and extra != 'group-15-stagehand-alpha-pydantic-v1' and extra != 'group-15-stagehand-alpha-pydantic-v2'", - "python_full_version < '3.10' and extra != 'group-15-stagehand-alpha-pydantic-v1' and extra != 'group-15-stagehand-alpha-pydantic-v2'", + "python_full_version >= '3.14' and extra != 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2'", + "python_full_version >= '3.10' and python_full_version < '3.14' and extra != 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2'", + "python_full_version < '3.10' and extra != 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2'", + "python_full_version >= '3.10' and extra != 'group-9-stagehand-pydantic-v1' and extra != 'group-9-stagehand-pydantic-v2'", + "python_full_version < '3.10' and extra != 'group-9-stagehand-pydantic-v1' and extra != 'group-9-stagehand-pydantic-v2'", ] dependencies = [ - { name = "annotated-types", marker = "extra == 'group-15-stagehand-alpha-pydantic-v2' or extra != 'group-15-stagehand-alpha-pydantic-v1'" }, - { name = "pydantic-core", marker = "extra == 'group-15-stagehand-alpha-pydantic-v2' or extra != 'group-15-stagehand-alpha-pydantic-v1'" }, - { name = "typing-extensions", marker = "extra == 'group-15-stagehand-alpha-pydantic-v2' or extra != 'group-15-stagehand-alpha-pydantic-v1'" }, - { name = "typing-inspection", marker = "extra == 'group-15-stagehand-alpha-pydantic-v2' or extra != 'group-15-stagehand-alpha-pydantic-v1'" }, + { name = "annotated-types", marker = "extra == 'group-9-stagehand-pydantic-v2' or extra != 'group-9-stagehand-pydantic-v1'" }, + { name = "pydantic-core", marker = "extra == 'group-9-stagehand-pydantic-v2' or extra != 'group-9-stagehand-pydantic-v1'" }, + { name = "typing-extensions", marker = "extra == 'group-9-stagehand-pydantic-v2' or extra != 'group-9-stagehand-pydantic-v1'" }, + { name = "typing-inspection", marker = "extra == 'group-9-stagehand-pydantic-v2' or extra != 'group-9-stagehand-pydantic-v1'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591, upload-time = "2025-11-26T15:11:46.471Z" } wheels = [ @@ -1011,7 +1011,7 @@ name = "pydantic-core" version = "2.41.5" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions", marker = "extra == 'group-15-stagehand-alpha-pydantic-v2' or extra != 'group-15-stagehand-alpha-pydantic-v1'" }, + { name = "typing-extensions", marker = "extra == 'group-9-stagehand-pydantic-v2' or extra != 'group-9-stagehand-pydantic-v1'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" } wheels = [ @@ -1167,13 +1167,13 @@ resolution-markers = [ "python_full_version < '3.10'", ] dependencies = [ - { name = "colorama", marker = "(python_full_version < '3.10' and sys_platform == 'win32') or (python_full_version >= '3.10' and extra == 'group-15-stagehand-alpha-pydantic-v1' and extra == 'group-15-stagehand-alpha-pydantic-v2') or (sys_platform != 'win32' and extra == 'group-15-stagehand-alpha-pydantic-v1' and extra == 'group-15-stagehand-alpha-pydantic-v2')" }, - { name = "exceptiongroup", marker = "python_full_version < '3.10' or (extra == 'group-15-stagehand-alpha-pydantic-v1' and extra == 'group-15-stagehand-alpha-pydantic-v2')" }, - { name = "iniconfig", version = "2.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10' or (extra == 'group-15-stagehand-alpha-pydantic-v1' and extra == 'group-15-stagehand-alpha-pydantic-v2')" }, - { name = "packaging", marker = "python_full_version < '3.10' or (extra == 'group-15-stagehand-alpha-pydantic-v1' and extra == 'group-15-stagehand-alpha-pydantic-v2')" }, - { name = "pluggy", marker = "python_full_version < '3.10' or (extra == 'group-15-stagehand-alpha-pydantic-v1' and extra == 'group-15-stagehand-alpha-pydantic-v2')" }, - { name = "pygments", marker = "python_full_version < '3.10' or (extra == 'group-15-stagehand-alpha-pydantic-v1' and extra == 'group-15-stagehand-alpha-pydantic-v2')" }, - { name = "tomli", marker = "python_full_version < '3.10' or (extra == 'group-15-stagehand-alpha-pydantic-v1' and extra == 'group-15-stagehand-alpha-pydantic-v2')" }, + { name = "colorama", marker = "(python_full_version < '3.10' and sys_platform == 'win32') or (python_full_version >= '3.10' and extra == 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2') or (sys_platform != 'win32' and extra == 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2')" }, + { name = "exceptiongroup", marker = "python_full_version < '3.10' or (extra == 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2')" }, + { name = "iniconfig", version = "2.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10' or (extra == 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2')" }, + { name = "packaging", marker = "python_full_version < '3.10' or (extra == 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2')" }, + { name = "pluggy", marker = "python_full_version < '3.10' or (extra == 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2')" }, + { name = "pygments", marker = "python_full_version < '3.10' or (extra == 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2')" }, + { name = "tomli", marker = "python_full_version < '3.10' or (extra == 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" } wheels = [ @@ -1185,19 +1185,19 @@ name = "pytest" version = "9.0.1" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.14' and extra != 'group-15-stagehand-alpha-pydantic-v1' and extra == 'group-15-stagehand-alpha-pydantic-v2'", - "python_full_version >= '3.10' and python_full_version < '3.14' and extra != 'group-15-stagehand-alpha-pydantic-v1' and extra == 'group-15-stagehand-alpha-pydantic-v2'", - "python_full_version >= '3.10' and extra == 'group-15-stagehand-alpha-pydantic-v1' and extra != 'group-15-stagehand-alpha-pydantic-v2'", - "python_full_version >= '3.10' and extra != 'group-15-stagehand-alpha-pydantic-v1' and extra != 'group-15-stagehand-alpha-pydantic-v2'", + "python_full_version >= '3.14' and extra != 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2'", + "python_full_version >= '3.10' and python_full_version < '3.14' and extra != 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2'", + "python_full_version >= '3.10' and extra == 'group-9-stagehand-pydantic-v1' and extra != 'group-9-stagehand-pydantic-v2'", + "python_full_version >= '3.10' and extra != 'group-9-stagehand-pydantic-v1' and extra != 'group-9-stagehand-pydantic-v2'", ] dependencies = [ - { name = "colorama", marker = "(python_full_version >= '3.10' and sys_platform == 'win32') or (python_full_version < '3.10' and extra == 'group-15-stagehand-alpha-pydantic-v1' and extra == 'group-15-stagehand-alpha-pydantic-v2') or (sys_platform != 'win32' and extra == 'group-15-stagehand-alpha-pydantic-v1' and extra == 'group-15-stagehand-alpha-pydantic-v2')" }, - { name = "exceptiongroup", marker = "python_full_version == '3.10.*' or (extra == 'group-15-stagehand-alpha-pydantic-v1' and extra == 'group-15-stagehand-alpha-pydantic-v2')" }, - { name = "iniconfig", version = "2.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10' or (extra == 'group-15-stagehand-alpha-pydantic-v1' and extra == 'group-15-stagehand-alpha-pydantic-v2')" }, - { name = "packaging", marker = "python_full_version >= '3.10' or (extra == 'group-15-stagehand-alpha-pydantic-v1' and extra == 'group-15-stagehand-alpha-pydantic-v2')" }, - { name = "pluggy", marker = "python_full_version >= '3.10' or (extra == 'group-15-stagehand-alpha-pydantic-v1' and extra == 'group-15-stagehand-alpha-pydantic-v2')" }, - { name = "pygments", marker = "python_full_version >= '3.10' or (extra == 'group-15-stagehand-alpha-pydantic-v1' and extra == 'group-15-stagehand-alpha-pydantic-v2')" }, - { name = "tomli", marker = "python_full_version == '3.10.*' or (extra == 'group-15-stagehand-alpha-pydantic-v1' and extra == 'group-15-stagehand-alpha-pydantic-v2')" }, + { name = "colorama", marker = "(python_full_version >= '3.10' and sys_platform == 'win32') or (python_full_version < '3.10' and extra == 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2') or (sys_platform != 'win32' and extra == 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2')" }, + { name = "exceptiongroup", marker = "python_full_version == '3.10.*' or (extra == 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2')" }, + { name = "iniconfig", version = "2.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10' or (extra == 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2')" }, + { name = "packaging", marker = "python_full_version >= '3.10' or (extra == 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2')" }, + { name = "pluggy", marker = "python_full_version >= '3.10' or (extra == 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2')" }, + { name = "pygments", marker = "python_full_version >= '3.10' or (extra == 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2')" }, + { name = "tomli", marker = "python_full_version == '3.10.*' or (extra == 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/07/56/f013048ac4bc4c1d9be45afd4ab209ea62822fb1598f40687e6bf45dcea4/pytest-9.0.1.tar.gz", hash = "sha256:3e9c069ea73583e255c3b21cf46b8d3c56f6e3a1a8f6da94ccb0fcf57b9d73c8", size = 1564125, upload-time = "2025-11-12T13:05:09.333Z" } wheels = [ @@ -1212,9 +1212,9 @@ resolution-markers = [ "python_full_version < '3.10'", ] dependencies = [ - { name = "backports-asyncio-runner", marker = "python_full_version < '3.10' or (extra == 'group-15-stagehand-alpha-pydantic-v1' and extra == 'group-15-stagehand-alpha-pydantic-v2')" }, - { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10' or (extra == 'group-15-stagehand-alpha-pydantic-v1' and extra == 'group-15-stagehand-alpha-pydantic-v2')" }, - { name = "typing-extensions", marker = "python_full_version < '3.10' or (extra == 'group-15-stagehand-alpha-pydantic-v1' and extra == 'group-15-stagehand-alpha-pydantic-v2')" }, + { name = "backports-asyncio-runner", marker = "python_full_version < '3.10' or (extra == 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2')" }, + { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10' or (extra == 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2')" }, + { name = "typing-extensions", marker = "python_full_version < '3.10' or (extra == 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/42/86/9e3c5f48f7b7b638b216e4b9e645f54d199d7abbbab7a64a13b4e12ba10f/pytest_asyncio-1.2.0.tar.gz", hash = "sha256:c609a64a2a8768462d0c99811ddb8bd2583c33fd33cf7f21af1c142e824ffb57", size = 50119, upload-time = "2025-09-12T07:33:53.816Z" } wheels = [ @@ -1226,15 +1226,15 @@ name = "pytest-asyncio" version = "1.3.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.14' and extra != 'group-15-stagehand-alpha-pydantic-v1' and extra == 'group-15-stagehand-alpha-pydantic-v2'", - "python_full_version >= '3.10' and python_full_version < '3.14' and extra != 'group-15-stagehand-alpha-pydantic-v1' and extra == 'group-15-stagehand-alpha-pydantic-v2'", - "python_full_version >= '3.10' and extra == 'group-15-stagehand-alpha-pydantic-v1' and extra != 'group-15-stagehand-alpha-pydantic-v2'", - "python_full_version >= '3.10' and extra != 'group-15-stagehand-alpha-pydantic-v1' and extra != 'group-15-stagehand-alpha-pydantic-v2'", + "python_full_version >= '3.14' and extra != 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2'", + "python_full_version >= '3.10' and python_full_version < '3.14' and extra != 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2'", + "python_full_version >= '3.10' and extra == 'group-9-stagehand-pydantic-v1' and extra != 'group-9-stagehand-pydantic-v2'", + "python_full_version >= '3.10' and extra != 'group-9-stagehand-pydantic-v1' and extra != 'group-9-stagehand-pydantic-v2'", ] dependencies = [ - { name = "backports-asyncio-runner", marker = "python_full_version == '3.10.*' or (extra == 'group-15-stagehand-alpha-pydantic-v1' and extra == 'group-15-stagehand-alpha-pydantic-v2')" }, - { name = "pytest", version = "9.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10' or (extra == 'group-15-stagehand-alpha-pydantic-v1' and extra == 'group-15-stagehand-alpha-pydantic-v2')" }, - { name = "typing-extensions", marker = "(python_full_version >= '3.10' and python_full_version < '3.13') or (python_full_version < '3.10' and extra == 'group-15-stagehand-alpha-pydantic-v1' and extra == 'group-15-stagehand-alpha-pydantic-v2') or (python_full_version >= '3.13' and extra == 'group-15-stagehand-alpha-pydantic-v1' and extra == 'group-15-stagehand-alpha-pydantic-v2')" }, + { name = "backports-asyncio-runner", marker = "python_full_version == '3.10.*' or (extra == 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2')" }, + { name = "pytest", version = "9.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10' or (extra == 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2')" }, + { name = "typing-extensions", marker = "(python_full_version >= '3.10' and python_full_version < '3.13') or (python_full_version < '3.10' and extra == 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2') or (python_full_version >= '3.13' and extra == 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/90/2c/8af215c0f776415f3590cac4f9086ccefd6fd463befeae41cd4d3f193e5a/pytest_asyncio-1.3.0.tar.gz", hash = "sha256:d7f52f36d231b80ee124cd216ffb19369aa168fc10095013c6b014a34d3ee9e5", size = 50087, upload-time = "2025-11-10T16:07:47.256Z" } wheels = [ @@ -1247,8 +1247,8 @@ version = "3.8.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "execnet" }, - { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10' or (extra == 'group-15-stagehand-alpha-pydantic-v1' and extra == 'group-15-stagehand-alpha-pydantic-v2')" }, - { name = "pytest", version = "9.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10' or (extra == 'group-15-stagehand-alpha-pydantic-v1' and extra == 'group-15-stagehand-alpha-pydantic-v2')" }, + { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10' or (extra == 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2')" }, + { name = "pytest", version = "9.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10' or (extra == 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/78/b4/439b179d1ff526791eb921115fca8e44e596a13efeda518b9d845a619450/pytest_xdist-3.8.0.tar.gz", hash = "sha256:7e578125ec9bc6050861aa93f2d59f1d8d085595d6551c2c90b6f4fad8d3a9f1", size = 88069, upload-time = "2025-07-01T13:30:59.346Z" } wheels = [ @@ -1260,7 +1260,7 @@ name = "python-dateutil" version = "2.9.0.post0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "six", marker = "python_full_version < '3.10' or (extra == 'group-15-stagehand-alpha-pydantic-v1' and extra == 'group-15-stagehand-alpha-pydantic-v2')" }, + { name = "six", marker = "python_full_version < '3.10' or (extra == 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } wheels = [ @@ -1284,8 +1284,8 @@ name = "rich" version = "14.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "markdown-it-py", version = "3.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10' or (extra == 'group-15-stagehand-alpha-pydantic-v1' and extra == 'group-15-stagehand-alpha-pydantic-v2')" }, - { name = "markdown-it-py", version = "4.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10' or (extra == 'group-15-stagehand-alpha-pydantic-v1' and extra == 'group-15-stagehand-alpha-pydantic-v2')" }, + { name = "markdown-it-py", version = "3.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10' or (extra == 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2')" }, + { name = "markdown-it-py", version = "4.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10' or (extra == 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2')" }, { name = "pygments" }, ] sdist = { url = "https://files.pythonhosted.org/packages/fb/d2/8920e102050a0de7bfabeb4c4614a49248cf8d5d7a8d01885fbb24dc767a/rich-14.2.0.tar.gz", hash = "sha256:73ff50c7c0c1c77c8243079283f4edb376f0f6442433aecb8ce7e6d0b92d1fe4", size = 219990, upload-time = "2025-10-09T14:16:53.064Z" } @@ -1338,15 +1338,15 @@ wheels = [ ] [[package]] -name = "stagehand-alpha" +name = "stagehand" version = "0.3.1" source = { editable = "." } dependencies = [ { name = "anyio" }, { name = "distro" }, { name = "httpx" }, - { name = "pydantic", version = "1.10.24", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'group-15-stagehand-alpha-pydantic-v1'" }, - { name = "pydantic", version = "2.12.5", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'group-15-stagehand-alpha-pydantic-v2' or extra != 'group-15-stagehand-alpha-pydantic-v1'" }, + { name = "pydantic", version = "1.10.24", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'group-9-stagehand-pydantic-v1'" }, + { name = "pydantic", version = "2.12.5", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'group-9-stagehand-pydantic-v2' or extra != 'group-9-stagehand-pydantic-v1'" }, { name = "sniffio" }, { name = "typing-extensions" }, ] @@ -1363,16 +1363,16 @@ dev = [ { name = "importlib-metadata" }, { name = "mypy" }, { name = "pyright" }, - { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10' or (extra == 'group-15-stagehand-alpha-pydantic-v1' and extra == 'group-15-stagehand-alpha-pydantic-v2')" }, - { name = "pytest", version = "9.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10' or (extra == 'group-15-stagehand-alpha-pydantic-v1' and extra == 'group-15-stagehand-alpha-pydantic-v2')" }, - { name = "pytest-asyncio", version = "1.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10' or (extra == 'group-15-stagehand-alpha-pydantic-v1' and extra == 'group-15-stagehand-alpha-pydantic-v2')" }, - { name = "pytest-asyncio", version = "1.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10' or (extra == 'group-15-stagehand-alpha-pydantic-v1' and extra == 'group-15-stagehand-alpha-pydantic-v2')" }, + { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10' or (extra == 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2')" }, + { name = "pytest", version = "9.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10' or (extra == 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2')" }, + { name = "pytest-asyncio", version = "1.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10' or (extra == 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2')" }, + { name = "pytest-asyncio", version = "1.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10' or (extra == 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2')" }, { name = "pytest-xdist" }, { name = "respx" }, { name = "rich" }, { name = "ruff" }, - { name = "time-machine", version = "2.19.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10' or (extra == 'group-15-stagehand-alpha-pydantic-v1' and extra == 'group-15-stagehand-alpha-pydantic-v2')" }, - { name = "time-machine", version = "3.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10' or (extra == 'group-15-stagehand-alpha-pydantic-v1' and extra == 'group-15-stagehand-alpha-pydantic-v2')" }, + { name = "time-machine", version = "2.19.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10' or (extra == 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2')" }, + { name = "time-machine", version = "3.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10' or (extra == 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2')" }, ] pydantic-v1 = [ { name = "pydantic", version = "1.10.24", source = { registry = "https://pypi.org/simple" } }, @@ -1422,7 +1422,7 @@ resolution-markers = [ "python_full_version < '3.10'", ] dependencies = [ - { name = "python-dateutil", marker = "python_full_version < '3.10' or (extra == 'group-15-stagehand-alpha-pydantic-v1' and extra == 'group-15-stagehand-alpha-pydantic-v2')" }, + { name = "python-dateutil", marker = "python_full_version < '3.10' or (extra == 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/f8/a4/1b5fdd165f61b67f445fac2a7feb0c655118edef429cd09ff5a8067f7f1d/time_machine-2.19.0.tar.gz", hash = "sha256:7c5065a8b3f2bbb449422c66ef71d114d3f909c276a6469642ecfffb6a0fcd29", size = 14576, upload-time = "2025-08-19T17:22:08.402Z" } wheels = [ @@ -1521,10 +1521,10 @@ name = "time-machine" version = "3.1.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.14' and extra != 'group-15-stagehand-alpha-pydantic-v1' and extra == 'group-15-stagehand-alpha-pydantic-v2'", - "python_full_version >= '3.10' and python_full_version < '3.14' and extra != 'group-15-stagehand-alpha-pydantic-v1' and extra == 'group-15-stagehand-alpha-pydantic-v2'", - "python_full_version >= '3.10' and extra == 'group-15-stagehand-alpha-pydantic-v1' and extra != 'group-15-stagehand-alpha-pydantic-v2'", - "python_full_version >= '3.10' and extra != 'group-15-stagehand-alpha-pydantic-v1' and extra != 'group-15-stagehand-alpha-pydantic-v2'", + "python_full_version >= '3.14' and extra != 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2'", + "python_full_version >= '3.10' and python_full_version < '3.14' and extra != 'group-9-stagehand-pydantic-v1' and extra == 'group-9-stagehand-pydantic-v2'", + "python_full_version >= '3.10' and extra == 'group-9-stagehand-pydantic-v1' and extra != 'group-9-stagehand-pydantic-v2'", + "python_full_version >= '3.10' and extra != 'group-9-stagehand-pydantic-v1' and extra != 'group-9-stagehand-pydantic-v2'", ] sdist = { url = "https://files.pythonhosted.org/packages/17/bd/a1bb03eb39ce35c966f0dde6559df7348ca0580f7cd3a956fdd7ed0b5080/time_machine-3.1.0.tar.gz", hash = "sha256:90831c2cf9e18e4199abb85fafa0c0ca0c6c15d0894a03ef68d5005a796c4f27", size = 14436, upload-time = "2025-11-21T13:56:33.802Z" } wheels = [ @@ -1670,7 +1670,7 @@ name = "typing-inspection" version = "0.4.2" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions", marker = "extra == 'group-15-stagehand-alpha-pydantic-v2' or extra != 'group-15-stagehand-alpha-pydantic-v1'" }, + { name = "typing-extensions", marker = "extra == 'group-9-stagehand-pydantic-v2' or extra != 'group-9-stagehand-pydantic-v1'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } wheels = [ From 5c95bb0d6da31c099f910ab730e31948d1b22098 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 13 Jan 2026 18:09:42 +0000 Subject: [PATCH 68/88] chore: update SDK settings --- .github/workflows/release-doctor.yml | 2 +- .stats.yml | 2 +- CONTRIBUTING.md | 2 +- README.md | 4 ++-- pyproject.toml | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/release-doctor.yml b/.github/workflows/release-doctor.yml index 2caaf57f..23563b96 100644 --- a/.github/workflows/release-doctor.yml +++ b/.github/workflows/release-doctor.yml @@ -2,7 +2,7 @@ name: Release Doctor on: pull_request: branches: - - stainless + - main workflow_dispatch: jobs: diff --git a/.stats.yml b/.stats.yml index 9affb565..d6bcc318 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 7 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fstagehand-991d1530002115ecec027f98cad357d39ca1ece6784f62d48e6740b8830e1104.yml openapi_spec_hash: 303329893ced56b2c129fb9fd666144e -config_hash: 6638c79b9a4c739a7f1154eb16321d48 +config_hash: 1bbded545069024fcb805e1f6c6a9985 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f29de26b..d42a6f15 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -62,7 +62,7 @@ If you’d like to use the repository from source, you can either install from g To install via git: ```sh -$ pip install git+ssh://git@github.com/browserbase/stagehand-python#stainless.git +$ pip install git+ssh://git@github.com/browserbase/stagehand-python.git ``` Alternatively, you can build from source and install the wheel file: diff --git a/README.md b/README.md index a0a19e88..d80419b1 100644 --- a/README.md +++ b/README.md @@ -331,9 +331,9 @@ session = response.parse() # get the object that `sessions.start()` would have print(session.data) ``` -These methods return an [`APIResponse`](https://github.com/browserbase/stagehand-python/tree/stainless/src/stagehand/_response.py) object. +These methods return an [`APIResponse`](https://github.com/browserbase/stagehand-python/tree/main/src/stagehand/_response.py) object. -The async client returns an [`AsyncAPIResponse`](https://github.com/browserbase/stagehand-python/tree/stainless/src/stagehand/_response.py) with the same structure, the only difference being `await`able methods for reading the response content. +The async client returns an [`AsyncAPIResponse`](https://github.com/browserbase/stagehand-python/tree/main/src/stagehand/_response.py) with the same structure, the only difference being `await`able methods for reading the response content. #### `.with_streaming_response` diff --git a/pyproject.toml b/pyproject.toml index 5c7955de..87e7c9cd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -112,7 +112,7 @@ path = "README.md" [[tool.hatch.metadata.hooks.fancy-pypi-readme.substitutions]] # replace relative links with absolute links pattern = '\[(.+?)\]\(((?!https?://)\S+?)\)' -replacement = '[\1](https://github.com/browserbase/stagehand-python/tree/stainless/\g<2>)' +replacement = '[\1](https://github.com/browserbase/stagehand-python/tree/main/\g<2>)' [tool.pytest.ini_options] testpaths = ["tests"] From f9c68cf31832158c075fbcaf16df036c31ebee4a Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 13 Jan 2026 18:37:51 +0000 Subject: [PATCH 69/88] chore(internal): version bump --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/stagehand/_version.py | 2 +- uv.lock | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index cce92405..da59f99e 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.3.1" + ".": "0.4.0" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 87e7c9cd..a613c50e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "stagehand" -version = "0.3.1" +version = "0.4.0" description = "The official Python library for the stagehand API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/stagehand/_version.py b/src/stagehand/_version.py index 76d534d7..c26830d4 100644 --- a/src/stagehand/_version.py +++ b/src/stagehand/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "stagehand" -__version__ = "0.3.1" # x-release-please-version +__version__ = "0.4.0" # x-release-please-version diff --git a/uv.lock b/uv.lock index 94d26518..0cfce0fd 100644 --- a/uv.lock +++ b/uv.lock @@ -1339,7 +1339,7 @@ wheels = [ [[package]] name = "stagehand" -version = "0.3.1" +version = "0.4.0" source = { editable = "." } dependencies = [ { name = "anyio" }, From 940833aaaa10cc09b93c3bfcfb4e66d25fc79fc9 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 13 Jan 2026 18:54:48 +0000 Subject: [PATCH 70/88] chore(internal): version bump --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/stagehand/_version.py | 2 +- uv.lock | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index da59f99e..dd88ece2 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.4.0" + ".": "3.4.1" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index a613c50e..9b2ae8a3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "stagehand" -version = "0.4.0" +version = "3.4.1" description = "The official Python library for the stagehand API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/stagehand/_version.py b/src/stagehand/_version.py index c26830d4..77a13513 100644 --- a/src/stagehand/_version.py +++ b/src/stagehand/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "stagehand" -__version__ = "0.4.0" # x-release-please-version +__version__ = "3.4.1" # x-release-please-version diff --git a/uv.lock b/uv.lock index 0cfce0fd..6aeeb8c8 100644 --- a/uv.lock +++ b/uv.lock @@ -1339,7 +1339,7 @@ wheels = [ [[package]] name = "stagehand" -version = "0.4.0" +version = "3.4.1" source = { editable = "." } dependencies = [ { name = "anyio" }, From 4f5abeab4094892918ea82c146bfe0a20bdc1b6f Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 13 Jan 2026 19:05:00 +0000 Subject: [PATCH 71/88] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index d6bcc318..156799ba 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 7 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fstagehand-991d1530002115ecec027f98cad357d39ca1ece6784f62d48e6740b8830e1104.yml openapi_spec_hash: 303329893ced56b2c129fb9fd666144e -config_hash: 1bbded545069024fcb805e1f6c6a9985 +config_hash: 2357dc3b80abe40de8d2dc4885928306 From 5f3789a68530d8841c3a163e05018b612cb031e6 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 13 Jan 2026 19:17:36 +0000 Subject: [PATCH 72/88] chore(internal): version bump --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/stagehand/_version.py | 2 +- uv.lock | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index dd88ece2..acdf5706 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "3.4.1" + ".": "3.4.2" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 9b2ae8a3..337cc6cb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "stagehand" -version = "3.4.1" +version = "3.4.2" description = "The official Python library for the stagehand API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/stagehand/_version.py b/src/stagehand/_version.py index 77a13513..af1992ec 100644 --- a/src/stagehand/_version.py +++ b/src/stagehand/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "stagehand" -__version__ = "3.4.1" # x-release-please-version +__version__ = "3.4.2" # x-release-please-version diff --git a/uv.lock b/uv.lock index 6aeeb8c8..3e5a28f8 100644 --- a/uv.lock +++ b/uv.lock @@ -1339,7 +1339,7 @@ wheels = [ [[package]] name = "stagehand" -version = "3.4.1" +version = "3.4.2" source = { editable = "." } dependencies = [ { name = "anyio" }, From df03e8029a338fa2d69fb2762cf5af4e98261fd1 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 13 Jan 2026 20:02:52 +0000 Subject: [PATCH 73/88] chore(internal): version bump --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/stagehand/_version.py | 2 +- uv.lock | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index acdf5706..00ba6719 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "3.4.2" + ".": "3.4.3" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 337cc6cb..978bc8b1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "stagehand" -version = "3.4.2" +version = "3.4.3" description = "The official Python library for the stagehand API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/stagehand/_version.py b/src/stagehand/_version.py index af1992ec..74516dbc 100644 --- a/src/stagehand/_version.py +++ b/src/stagehand/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "stagehand" -__version__ = "3.4.2" # x-release-please-version +__version__ = "3.4.3" # x-release-please-version diff --git a/uv.lock b/uv.lock index 3e5a28f8..f5d88154 100644 --- a/uv.lock +++ b/uv.lock @@ -1339,7 +1339,7 @@ wheels = [ [[package]] name = "stagehand" -version = "3.4.2" +version = "3.4.3" source = { editable = "." } dependencies = [ { name = "anyio" }, From 87b229073bc6a1e6401391771ea9dd63d4bce527 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 13 Jan 2026 20:09:02 +0000 Subject: [PATCH 74/88] chore(internal): version bump --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/stagehand/_version.py | 2 +- uv.lock | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 00ba6719..0f967819 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "3.4.3" + ".": "3.4.4" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 978bc8b1..98086b2b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "stagehand" -version = "3.4.3" +version = "3.4.4" description = "The official Python library for the stagehand API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/stagehand/_version.py b/src/stagehand/_version.py index 74516dbc..aaca849d 100644 --- a/src/stagehand/_version.py +++ b/src/stagehand/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "stagehand" -__version__ = "3.4.3" # x-release-please-version +__version__ = "3.4.4" # x-release-please-version diff --git a/uv.lock b/uv.lock index f5d88154..eac5c536 100644 --- a/uv.lock +++ b/uv.lock @@ -1339,7 +1339,7 @@ wheels = [ [[package]] name = "stagehand" -version = "3.4.3" +version = "3.4.4" source = { editable = "." } dependencies = [ { name = "anyio" }, From 729ff2eaef626f71b72edee55208da8e5afe1ac3 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 13 Jan 2026 20:16:00 +0000 Subject: [PATCH 75/88] chore(internal): version bump --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/stagehand/_version.py | 2 +- uv.lock | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 0f967819..b96fa355 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "3.4.4" + ".": "3.4.5" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 98086b2b..57641203 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "stagehand" -version = "3.4.4" +version = "3.4.5" description = "The official Python library for the stagehand API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/stagehand/_version.py b/src/stagehand/_version.py index aaca849d..da33b49e 100644 --- a/src/stagehand/_version.py +++ b/src/stagehand/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "stagehand" -__version__ = "3.4.4" # x-release-please-version +__version__ = "3.4.5" # x-release-please-version diff --git a/uv.lock b/uv.lock index eac5c536..d3c5e839 100644 --- a/uv.lock +++ b/uv.lock @@ -1339,7 +1339,7 @@ wheels = [ [[package]] name = "stagehand" -version = "3.4.4" +version = "3.4.5" source = { editable = "." } dependencies = [ { name = "anyio" }, From 64e6167c61123baa04afc0e0d67280cead33342e Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 13 Jan 2026 20:30:22 +0000 Subject: [PATCH 76/88] chore(internal): version bump --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/stagehand/_version.py | 2 +- uv.lock | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index b96fa355..8ed03cbb 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "3.4.5" + ".": "3.4.6" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 57641203..e4d14b1c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "stagehand" -version = "3.4.5" +version = "3.4.6" description = "The official Python library for the stagehand API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/stagehand/_version.py b/src/stagehand/_version.py index da33b49e..70c78614 100644 --- a/src/stagehand/_version.py +++ b/src/stagehand/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "stagehand" -__version__ = "3.4.5" # x-release-please-version +__version__ = "3.4.6" # x-release-please-version diff --git a/uv.lock b/uv.lock index d3c5e839..7e6b822c 100644 --- a/uv.lock +++ b/uv.lock @@ -1339,7 +1339,7 @@ wheels = [ [[package]] name = "stagehand" -version = "3.4.5" +version = "3.4.6" source = { editable = "." } dependencies = [ { name = "anyio" }, From a6a4a8b3e39ff4845b8403a275894c9b837f6abf Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 14 Jan 2026 07:00:38 +0000 Subject: [PATCH 77/88] feat(client): add support for binary request streaming --- src/stagehand/_base_client.py | 145 ++++++++++++++++++++++++-- src/stagehand/_models.py | 17 ++- src/stagehand/_types.py | 9 ++ tests/test_client.py | 191 +++++++++++++++++++++++++++++++++- 4 files changed, 348 insertions(+), 14 deletions(-) diff --git a/src/stagehand/_base_client.py b/src/stagehand/_base_client.py index 921ca2f7..00e248e2 100644 --- a/src/stagehand/_base_client.py +++ b/src/stagehand/_base_client.py @@ -9,6 +9,7 @@ import inspect import logging import platform +import warnings import email.utils from types import TracebackType from random import random @@ -51,9 +52,11 @@ ResponseT, AnyMapping, PostParser, + BinaryTypes, RequestFiles, HttpxSendArgs, RequestOptions, + AsyncBinaryTypes, HttpxRequestFiles, ModelBuilderProtocol, not_given, @@ -477,8 +480,19 @@ def _build_request( retries_taken: int = 0, ) -> httpx.Request: if log.isEnabledFor(logging.DEBUG): - log.debug("Request options: %s", model_dump(options, exclude_unset=True)) - + log.debug( + "Request options: %s", + model_dump( + options, + exclude_unset=True, + # Pydantic v1 can't dump every type we support in content, so we exclude it for now. + exclude={ + "content", + } + if PYDANTIC_V1 + else {}, + ), + ) kwargs: dict[str, Any] = {} json_data = options.json_data @@ -532,7 +546,13 @@ def _build_request( is_body_allowed = options.method.lower() != "get" if is_body_allowed: - if isinstance(json_data, bytes): + if options.content is not None and json_data is not None: + raise TypeError("Passing both `content` and `json_data` is not supported") + if options.content is not None and files is not None: + raise TypeError("Passing both `content` and `files` is not supported") + if options.content is not None: + kwargs["content"] = options.content + elif isinstance(json_data, bytes): kwargs["content"] = json_data else: kwargs["json"] = json_data if is_given(json_data) else None @@ -1194,6 +1214,7 @@ def post( *, cast_to: Type[ResponseT], body: Body | None = None, + content: BinaryTypes | None = None, options: RequestOptions = {}, files: RequestFiles | None = None, stream: Literal[False] = False, @@ -1206,6 +1227,7 @@ def post( *, cast_to: Type[ResponseT], body: Body | None = None, + content: BinaryTypes | None = None, options: RequestOptions = {}, files: RequestFiles | None = None, stream: Literal[True], @@ -1219,6 +1241,7 @@ def post( *, cast_to: Type[ResponseT], body: Body | None = None, + content: BinaryTypes | None = None, options: RequestOptions = {}, files: RequestFiles | None = None, stream: bool, @@ -1231,13 +1254,25 @@ def post( *, cast_to: Type[ResponseT], body: Body | None = None, + content: BinaryTypes | None = None, options: RequestOptions = {}, files: RequestFiles | None = None, stream: bool = False, stream_cls: type[_StreamT] | None = None, ) -> ResponseT | _StreamT: + if body is not None and content is not None: + raise TypeError("Passing both `body` and `content` is not supported") + if files is not None and content is not None: + raise TypeError("Passing both `files` and `content` is not supported") + if isinstance(body, bytes): + warnings.warn( + "Passing raw bytes as `body` is deprecated and will be removed in a future version. " + "Please pass raw bytes via the `content` parameter instead.", + DeprecationWarning, + stacklevel=2, + ) opts = FinalRequestOptions.construct( - method="post", url=path, json_data=body, files=to_httpx_files(files), **options + method="post", url=path, json_data=body, content=content, files=to_httpx_files(files), **options ) return cast(ResponseT, self.request(cast_to, opts, stream=stream, stream_cls=stream_cls)) @@ -1247,11 +1282,23 @@ def patch( *, cast_to: Type[ResponseT], body: Body | None = None, + content: BinaryTypes | None = None, files: RequestFiles | None = None, options: RequestOptions = {}, ) -> ResponseT: + if body is not None and content is not None: + raise TypeError("Passing both `body` and `content` is not supported") + if files is not None and content is not None: + raise TypeError("Passing both `files` and `content` is not supported") + if isinstance(body, bytes): + warnings.warn( + "Passing raw bytes as `body` is deprecated and will be removed in a future version. " + "Please pass raw bytes via the `content` parameter instead.", + DeprecationWarning, + stacklevel=2, + ) opts = FinalRequestOptions.construct( - method="patch", url=path, json_data=body, files=to_httpx_files(files), **options + method="patch", url=path, json_data=body, content=content, files=to_httpx_files(files), **options ) return self.request(cast_to, opts) @@ -1261,11 +1308,23 @@ def put( *, cast_to: Type[ResponseT], body: Body | None = None, + content: BinaryTypes | None = None, files: RequestFiles | None = None, options: RequestOptions = {}, ) -> ResponseT: + if body is not None and content is not None: + raise TypeError("Passing both `body` and `content` is not supported") + if files is not None and content is not None: + raise TypeError("Passing both `files` and `content` is not supported") + if isinstance(body, bytes): + warnings.warn( + "Passing raw bytes as `body` is deprecated and will be removed in a future version. " + "Please pass raw bytes via the `content` parameter instead.", + DeprecationWarning, + stacklevel=2, + ) opts = FinalRequestOptions.construct( - method="put", url=path, json_data=body, files=to_httpx_files(files), **options + method="put", url=path, json_data=body, content=content, files=to_httpx_files(files), **options ) return self.request(cast_to, opts) @@ -1275,9 +1334,19 @@ def delete( *, cast_to: Type[ResponseT], body: Body | None = None, + content: BinaryTypes | None = None, options: RequestOptions = {}, ) -> ResponseT: - opts = FinalRequestOptions.construct(method="delete", url=path, json_data=body, **options) + if body is not None and content is not None: + raise TypeError("Passing both `body` and `content` is not supported") + if isinstance(body, bytes): + warnings.warn( + "Passing raw bytes as `body` is deprecated and will be removed in a future version. " + "Please pass raw bytes via the `content` parameter instead.", + DeprecationWarning, + stacklevel=2, + ) + opts = FinalRequestOptions.construct(method="delete", url=path, json_data=body, content=content, **options) return self.request(cast_to, opts) def get_api_list( @@ -1717,6 +1786,7 @@ async def post( *, cast_to: Type[ResponseT], body: Body | None = None, + content: AsyncBinaryTypes | None = None, files: RequestFiles | None = None, options: RequestOptions = {}, stream: Literal[False] = False, @@ -1729,6 +1799,7 @@ async def post( *, cast_to: Type[ResponseT], body: Body | None = None, + content: AsyncBinaryTypes | None = None, files: RequestFiles | None = None, options: RequestOptions = {}, stream: Literal[True], @@ -1742,6 +1813,7 @@ async def post( *, cast_to: Type[ResponseT], body: Body | None = None, + content: AsyncBinaryTypes | None = None, files: RequestFiles | None = None, options: RequestOptions = {}, stream: bool, @@ -1754,13 +1826,25 @@ async def post( *, cast_to: Type[ResponseT], body: Body | None = None, + content: AsyncBinaryTypes | None = None, files: RequestFiles | None = None, options: RequestOptions = {}, stream: bool = False, stream_cls: type[_AsyncStreamT] | None = None, ) -> ResponseT | _AsyncStreamT: + if body is not None and content is not None: + raise TypeError("Passing both `body` and `content` is not supported") + if files is not None and content is not None: + raise TypeError("Passing both `files` and `content` is not supported") + if isinstance(body, bytes): + warnings.warn( + "Passing raw bytes as `body` is deprecated and will be removed in a future version. " + "Please pass raw bytes via the `content` parameter instead.", + DeprecationWarning, + stacklevel=2, + ) opts = FinalRequestOptions.construct( - method="post", url=path, json_data=body, files=await async_to_httpx_files(files), **options + method="post", url=path, json_data=body, content=content, files=await async_to_httpx_files(files), **options ) return await self.request(cast_to, opts, stream=stream, stream_cls=stream_cls) @@ -1770,11 +1854,28 @@ async def patch( *, cast_to: Type[ResponseT], body: Body | None = None, + content: AsyncBinaryTypes | None = None, files: RequestFiles | None = None, options: RequestOptions = {}, ) -> ResponseT: + if body is not None and content is not None: + raise TypeError("Passing both `body` and `content` is not supported") + if files is not None and content is not None: + raise TypeError("Passing both `files` and `content` is not supported") + if isinstance(body, bytes): + warnings.warn( + "Passing raw bytes as `body` is deprecated and will be removed in a future version. " + "Please pass raw bytes via the `content` parameter instead.", + DeprecationWarning, + stacklevel=2, + ) opts = FinalRequestOptions.construct( - method="patch", url=path, json_data=body, files=await async_to_httpx_files(files), **options + method="patch", + url=path, + json_data=body, + content=content, + files=await async_to_httpx_files(files), + **options, ) return await self.request(cast_to, opts) @@ -1784,11 +1885,23 @@ async def put( *, cast_to: Type[ResponseT], body: Body | None = None, + content: AsyncBinaryTypes | None = None, files: RequestFiles | None = None, options: RequestOptions = {}, ) -> ResponseT: + if body is not None and content is not None: + raise TypeError("Passing both `body` and `content` is not supported") + if files is not None and content is not None: + raise TypeError("Passing both `files` and `content` is not supported") + if isinstance(body, bytes): + warnings.warn( + "Passing raw bytes as `body` is deprecated and will be removed in a future version. " + "Please pass raw bytes via the `content` parameter instead.", + DeprecationWarning, + stacklevel=2, + ) opts = FinalRequestOptions.construct( - method="put", url=path, json_data=body, files=await async_to_httpx_files(files), **options + method="put", url=path, json_data=body, content=content, files=await async_to_httpx_files(files), **options ) return await self.request(cast_to, opts) @@ -1798,9 +1911,19 @@ async def delete( *, cast_to: Type[ResponseT], body: Body | None = None, + content: AsyncBinaryTypes | None = None, options: RequestOptions = {}, ) -> ResponseT: - opts = FinalRequestOptions.construct(method="delete", url=path, json_data=body, **options) + if body is not None and content is not None: + raise TypeError("Passing both `body` and `content` is not supported") + if isinstance(body, bytes): + warnings.warn( + "Passing raw bytes as `body` is deprecated and will be removed in a future version. " + "Please pass raw bytes via the `content` parameter instead.", + DeprecationWarning, + stacklevel=2, + ) + opts = FinalRequestOptions.construct(method="delete", url=path, json_data=body, content=content, **options) return await self.request(cast_to, opts) def get_api_list( diff --git a/src/stagehand/_models.py b/src/stagehand/_models.py index ca9500b2..29070e05 100644 --- a/src/stagehand/_models.py +++ b/src/stagehand/_models.py @@ -3,7 +3,20 @@ import os import inspect import weakref -from typing import TYPE_CHECKING, Any, Type, Union, Generic, TypeVar, Callable, Optional, cast +from typing import ( + IO, + TYPE_CHECKING, + Any, + Type, + Union, + Generic, + TypeVar, + Callable, + Iterable, + Optional, + AsyncIterable, + cast, +) from datetime import date, datetime from typing_extensions import ( List, @@ -787,6 +800,7 @@ class FinalRequestOptionsInput(TypedDict, total=False): timeout: float | Timeout | None files: HttpxRequestFiles | None idempotency_key: str + content: Union[bytes, bytearray, IO[bytes], Iterable[bytes], AsyncIterable[bytes], None] json_data: Body extra_json: AnyMapping follow_redirects: bool @@ -805,6 +819,7 @@ class FinalRequestOptions(pydantic.BaseModel): post_parser: Union[Callable[[Any], Any], NotGiven] = NotGiven() follow_redirects: Union[bool, None] = None + content: Union[bytes, bytearray, IO[bytes], Iterable[bytes], AsyncIterable[bytes], None] = None # It should be noted that we cannot use `json` here as that would override # a BaseModel method in an incompatible fashion. json_data: Union[Body, None] = None diff --git a/src/stagehand/_types.py b/src/stagehand/_types.py index c2ce0d7a..d25d0560 100644 --- a/src/stagehand/_types.py +++ b/src/stagehand/_types.py @@ -13,9 +13,11 @@ Mapping, TypeVar, Callable, + Iterable, Iterator, Optional, Sequence, + AsyncIterable, ) from typing_extensions import ( Set, @@ -56,6 +58,13 @@ else: Base64FileInput = Union[IO[bytes], PathLike] FileContent = Union[IO[bytes], bytes, PathLike] # PathLike is not subscriptable in Python 3.8. + + +# Used for sending raw binary data / streaming data in request bodies +# e.g. for file uploads without multipart encoding +BinaryTypes = Union[bytes, bytearray, IO[bytes], Iterable[bytes]] +AsyncBinaryTypes = Union[bytes, bytearray, IO[bytes], AsyncIterable[bytes]] + FileTypes = Union[ # file (or bytes) FileContent, diff --git a/tests/test_client.py b/tests/test_client.py index b04449bf..71a059e9 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -8,10 +8,11 @@ import json import asyncio import inspect +import dataclasses import tracemalloc -from typing import Any, Union, cast +from typing import Any, Union, TypeVar, Callable, Iterable, Iterator, Optional, Coroutine, cast from unittest import mock -from typing_extensions import Literal +from typing_extensions import Literal, AsyncIterator, override import httpx import pytest @@ -37,6 +38,7 @@ from .utils import update_env +T = TypeVar("T") base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") browserbase_api_key = "My Browserbase API Key" browserbase_project_id = "My Browserbase Project ID" @@ -53,6 +55,57 @@ def _low_retry_timeout(*_args: Any, **_kwargs: Any) -> float: return 0.1 +def mirror_request_content(request: httpx.Request) -> httpx.Response: + return httpx.Response(200, content=request.content) + + +# note: we can't use the httpx.MockTransport class as it consumes the request +# body itself, which means we can't test that the body is read lazily +class MockTransport(httpx.BaseTransport, httpx.AsyncBaseTransport): + def __init__( + self, + handler: Callable[[httpx.Request], httpx.Response] + | Callable[[httpx.Request], Coroutine[Any, Any, httpx.Response]], + ) -> None: + self.handler = handler + + @override + def handle_request( + self, + request: httpx.Request, + ) -> httpx.Response: + assert not inspect.iscoroutinefunction(self.handler), "handler must not be a coroutine function" + assert inspect.isfunction(self.handler), "handler must be a function" + return self.handler(request) + + @override + async def handle_async_request( + self, + request: httpx.Request, + ) -> httpx.Response: + assert inspect.iscoroutinefunction(self.handler), "handler must be a coroutine function" + return await self.handler(request) + + +@dataclasses.dataclass +class Counter: + value: int = 0 + + +def _make_sync_iterator(iterable: Iterable[T], counter: Optional[Counter] = None) -> Iterator[T]: + for item in iterable: + if counter: + counter.value += 1 + yield item + + +async def _make_async_iterator(iterable: Iterable[T], counter: Optional[Counter] = None) -> AsyncIterator[T]: + for item in iterable: + if counter: + counter.value += 1 + yield item + + def _get_open_connections(client: Stagehand | AsyncStagehand) -> int: transport = client._client._transport assert isinstance(transport, httpx.HTTPTransport) or isinstance(transport, httpx.AsyncHTTPTransport) @@ -579,6 +632,72 @@ def test_multipart_repeating_array(self, client: Stagehand) -> None: b"", ] + @pytest.mark.respx(base_url=base_url) + def test_binary_content_upload(self, respx_mock: MockRouter, client: Stagehand) -> None: + respx_mock.post("/upload").mock(side_effect=mirror_request_content) + + file_content = b"Hello, this is a test file." + + response = client.post( + "/upload", + content=file_content, + cast_to=httpx.Response, + options={"headers": {"Content-Type": "application/octet-stream"}}, + ) + + assert response.status_code == 200 + assert response.request.headers["Content-Type"] == "application/octet-stream" + assert response.content == file_content + + def test_binary_content_upload_with_iterator(self) -> None: + file_content = b"Hello, this is a test file." + counter = Counter() + iterator = _make_sync_iterator([file_content], counter=counter) + + def mock_handler(request: httpx.Request) -> httpx.Response: + assert counter.value == 0, "the request body should not have been read" + return httpx.Response(200, content=request.read()) + + with Stagehand( + base_url=base_url, + browserbase_api_key=browserbase_api_key, + browserbase_project_id=browserbase_project_id, + model_api_key=model_api_key, + _strict_response_validation=True, + http_client=httpx.Client(transport=MockTransport(handler=mock_handler)), + ) as client: + response = client.post( + "/upload", + content=iterator, + cast_to=httpx.Response, + options={"headers": {"Content-Type": "application/octet-stream"}}, + ) + + assert response.status_code == 200 + assert response.request.headers["Content-Type"] == "application/octet-stream" + assert response.content == file_content + assert counter.value == 1 + + @pytest.mark.respx(base_url=base_url) + def test_binary_content_upload_with_body_is_deprecated(self, respx_mock: MockRouter, client: Stagehand) -> None: + respx_mock.post("/upload").mock(side_effect=mirror_request_content) + + file_content = b"Hello, this is a test file." + + with pytest.deprecated_call( + match="Passing raw bytes as `body` is deprecated and will be removed in a future version. Please pass raw bytes via the `content` parameter instead." + ): + response = client.post( + "/upload", + body=file_content, + cast_to=httpx.Response, + options={"headers": {"Content-Type": "application/octet-stream"}}, + ) + + assert response.status_code == 200 + assert response.request.headers["Content-Type"] == "application/octet-stream" + assert response.content == file_content + @pytest.mark.respx(base_url=base_url) def test_basic_union_response(self, respx_mock: MockRouter, client: Stagehand) -> None: class Model1(BaseModel): @@ -1553,6 +1672,74 @@ def test_multipart_repeating_array(self, async_client: AsyncStagehand) -> None: b"", ] + @pytest.mark.respx(base_url=base_url) + async def test_binary_content_upload(self, respx_mock: MockRouter, async_client: AsyncStagehand) -> None: + respx_mock.post("/upload").mock(side_effect=mirror_request_content) + + file_content = b"Hello, this is a test file." + + response = await async_client.post( + "/upload", + content=file_content, + cast_to=httpx.Response, + options={"headers": {"Content-Type": "application/octet-stream"}}, + ) + + assert response.status_code == 200 + assert response.request.headers["Content-Type"] == "application/octet-stream" + assert response.content == file_content + + async def test_binary_content_upload_with_asynciterator(self) -> None: + file_content = b"Hello, this is a test file." + counter = Counter() + iterator = _make_async_iterator([file_content], counter=counter) + + async def mock_handler(request: httpx.Request) -> httpx.Response: + assert counter.value == 0, "the request body should not have been read" + return httpx.Response(200, content=await request.aread()) + + async with AsyncStagehand( + base_url=base_url, + browserbase_api_key=browserbase_api_key, + browserbase_project_id=browserbase_project_id, + model_api_key=model_api_key, + _strict_response_validation=True, + http_client=httpx.AsyncClient(transport=MockTransport(handler=mock_handler)), + ) as client: + response = await client.post( + "/upload", + content=iterator, + cast_to=httpx.Response, + options={"headers": {"Content-Type": "application/octet-stream"}}, + ) + + assert response.status_code == 200 + assert response.request.headers["Content-Type"] == "application/octet-stream" + assert response.content == file_content + assert counter.value == 1 + + @pytest.mark.respx(base_url=base_url) + async def test_binary_content_upload_with_body_is_deprecated( + self, respx_mock: MockRouter, async_client: AsyncStagehand + ) -> None: + respx_mock.post("/upload").mock(side_effect=mirror_request_content) + + file_content = b"Hello, this is a test file." + + with pytest.deprecated_call( + match="Passing raw bytes as `body` is deprecated and will be removed in a future version. Please pass raw bytes via the `content` parameter instead." + ): + response = await async_client.post( + "/upload", + body=file_content, + cast_to=httpx.Response, + options={"headers": {"Content-Type": "application/octet-stream"}}, + ) + + assert response.status_code == 200 + assert response.request.headers["Content-Type"] == "application/octet-stream" + assert response.content == file_content + @pytest.mark.respx(base_url=base_url) async def test_basic_union_response(self, respx_mock: MockRouter, async_client: AsyncStagehand) -> None: class Model1(BaseModel): From 466d95aa209e9f3f4b5d1bd99dd8fc70e9890615 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 14 Jan 2026 23:44:09 +0000 Subject: [PATCH 78/88] feat(api): manual updates --- .stats.yml | 2 +- LICENSE | 202 +------------------------------------------------ pyproject.toml | 4 +- 3 files changed, 7 insertions(+), 201 deletions(-) diff --git a/.stats.yml b/.stats.yml index 156799ba..01457a67 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 7 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fstagehand-991d1530002115ecec027f98cad357d39ca1ece6784f62d48e6740b8830e1104.yml openapi_spec_hash: 303329893ced56b2c129fb9fd666144e -config_hash: 2357dc3b80abe40de8d2dc4885928306 +config_hash: 3f1eb6ae44a4987a7c0c8a9d95035975 diff --git a/LICENSE b/LICENSE index d15d0212..a7b82c2f 100644 --- a/LICENSE +++ b/LICENSE @@ -1,201 +1,7 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ +Copyright 2026 stagehand - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION +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: - 1. Definitions. +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright 2026 Stagehand - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. +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/pyproject.toml b/pyproject.toml index e4d14b1c..118d6792 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,7 @@ name = "stagehand" version = "3.4.6" description = "The official Python library for the stagehand API" dynamic = ["readme"] -license = "Apache-2.0" +license = "MIT" authors = [ { name = "Stagehand", email = "" }, ] @@ -33,7 +33,7 @@ classifiers = [ "Operating System :: POSIX :: Linux", "Operating System :: Microsoft :: Windows", "Topic :: Software Development :: Libraries :: Python Modules", - "License :: OSI Approved :: Apache Software License" + "License :: OSI Approved :: MIT License" ] [project.urls] From 41d03c89466fc75f25eec2fb112aa42d9e07b733 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 15 Jan 2026 22:09:46 +0000 Subject: [PATCH 79/88] chore(internal): version bump --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/stagehand/_version.py | 2 +- uv.lock | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 8ed03cbb..06c9b658 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "3.4.6" + ".": "3.4.7" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 118d6792..f5a15105 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "stagehand" -version = "3.4.6" +version = "3.4.7" description = "The official Python library for the stagehand API" dynamic = ["readme"] license = "MIT" diff --git a/src/stagehand/_version.py b/src/stagehand/_version.py index 70c78614..ba209e07 100644 --- a/src/stagehand/_version.py +++ b/src/stagehand/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "stagehand" -__version__ = "3.4.6" # x-release-please-version +__version__ = "3.4.7" # x-release-please-version diff --git a/uv.lock b/uv.lock index 7e6b822c..d8f55f75 100644 --- a/uv.lock +++ b/uv.lock @@ -1339,7 +1339,7 @@ wheels = [ [[package]] name = "stagehand" -version = "3.4.6" +version = "3.4.7" source = { editable = "." } dependencies = [ { name = "anyio" }, From 998fbdc4ea3bf31cf735ea30a6a30fc5f65668c7 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 15 Jan 2026 22:13:05 +0000 Subject: [PATCH 80/88] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index 01457a67..2f45fa42 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 7 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fstagehand-991d1530002115ecec027f98cad357d39ca1ece6784f62d48e6740b8830e1104.yml openapi_spec_hash: 303329893ced56b2c129fb9fd666144e -config_hash: 3f1eb6ae44a4987a7c0c8a9d95035975 +config_hash: 0e6aecd610184fc5d011e30698df9a56 From 53762a71562134fba9eb6961f0dda1e7ee8d131a Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 15 Jan 2026 22:13:58 +0000 Subject: [PATCH 81/88] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index 2f45fa42..76507583 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 7 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fstagehand-991d1530002115ecec027f98cad357d39ca1ece6784f62d48e6740b8830e1104.yml openapi_spec_hash: 303329893ced56b2c129fb9fd666144e -config_hash: 0e6aecd610184fc5d011e30698df9a56 +config_hash: 3906b0eb870b3efb11f24b0069668c94 From 8d693fe4aea7a2c178da5aead64b5ca69b624250 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 15 Jan 2026 22:16:01 +0000 Subject: [PATCH 82/88] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index 76507583..f4e4a762 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 7 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fstagehand-991d1530002115ecec027f98cad357d39ca1ece6784f62d48e6740b8830e1104.yml openapi_spec_hash: 303329893ced56b2c129fb9fd666144e -config_hash: 3906b0eb870b3efb11f24b0069668c94 +config_hash: 3ad8653f1cf35720ac8f2f8643969e15 From fc7b074245d33e4d5bc40405d96b7593bab75970 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 15 Jan 2026 22:17:53 +0000 Subject: [PATCH 83/88] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index f4e4a762..936a1e97 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 7 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fstagehand-991d1530002115ecec027f98cad357d39ca1ece6784f62d48e6740b8830e1104.yml openapi_spec_hash: 303329893ced56b2c129fb9fd666144e -config_hash: 3ad8653f1cf35720ac8f2f8643969e15 +config_hash: d2eff937029f6230c90a974d0b64e024 From 8a5c4e6c10dbb956f5de169d87e550b31b2f3ec0 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 15 Jan 2026 22:19:41 +0000 Subject: [PATCH 84/88] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index 936a1e97..fbdd5097 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 7 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fstagehand-991d1530002115ecec027f98cad357d39ca1ece6784f62d48e6740b8830e1104.yml openapi_spec_hash: 303329893ced56b2c129fb9fd666144e -config_hash: d2eff937029f6230c90a974d0b64e024 +config_hash: dbb44d668902bdea601850a0db94302b From 9d0bbafb70544b6ba40f876fac82977bc137ccf9 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 15 Jan 2026 22:30:56 +0000 Subject: [PATCH 85/88] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index fbdd5097..8a75e9bd 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 7 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fstagehand-991d1530002115ecec027f98cad357d39ca1ece6784f62d48e6740b8830e1104.yml openapi_spec_hash: 303329893ced56b2c129fb9fd666144e -config_hash: dbb44d668902bdea601850a0db94302b +config_hash: e8739aa4b8fb23a89fbf1f3b1ba2a52f From de2f681fc3370e1a580317c660c58286a7a3c6a9 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 15 Jan 2026 22:37:09 +0000 Subject: [PATCH 86/88] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index 8a75e9bd..ee5126ee 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 7 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fstagehand-991d1530002115ecec027f98cad357d39ca1ece6784f62d48e6740b8830e1104.yml openapi_spec_hash: 303329893ced56b2c129fb9fd666144e -config_hash: e8739aa4b8fb23a89fbf1f3b1ba2a52f +config_hash: d4df55e4b30aac2d8d0982be97f837c4 From 8ba62c91275f20e5aa6aceaf9063205c17d7f806 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 16 Jan 2026 00:02:35 +0000 Subject: [PATCH 87/88] feat: x-stainless-any fix, optional frame id, ModelConfigString fix --- .stats.yml | 4 +- src/stagehand/resources/sessions.py | 247 ++++-------------- src/stagehand/types/model_config_param.py | 6 +- src/stagehand/types/session_act_params.py | 15 +- src/stagehand/types/session_end_params.py | 5 - src/stagehand/types/session_execute_params.py | 15 +- src/stagehand/types/session_extract_params.py | 15 +- .../types/session_navigate_params.py | 8 +- src/stagehand/types/session_observe_params.py | 15 +- src/stagehand/types/session_start_params.py | 10 +- tests/api_resources/test_sessions.py | 55 ++-- 11 files changed, 109 insertions(+), 286 deletions(-) diff --git a/.stats.yml b/.stats.yml index ee5126ee..ee1de314 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 7 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fstagehand-991d1530002115ecec027f98cad357d39ca1ece6784f62d48e6740b8830e1104.yml -openapi_spec_hash: 303329893ced56b2c129fb9fd666144e +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fstagehand-07032b695bc66ecd76328d936b41e01cfad508a870928c09c185f5faa5ea66ab.yml +openapi_spec_hash: fca4b895ce36ad547fb015c3dd38821f config_hash: d4df55e4b30aac2d8d0982be97f837c4 diff --git a/src/stagehand/resources/sessions.py b/src/stagehand/resources/sessions.py index 5f31c6b9..7bd10aad 100644 --- a/src/stagehand/resources/sessions.py +++ b/src/stagehand/resources/sessions.py @@ -2,8 +2,7 @@ from __future__ import annotations -from typing import Dict, Union -from datetime import datetime +from typing import Dict, Optional from typing_extensions import Literal, overload import httpx @@ -67,10 +66,9 @@ def act( id: str, *, input: session_act_params.Input, - frame_id: str | Omit = omit, + frame_id: Optional[str] | Omit = omit, options: session_act_params.Options | Omit = omit, stream_response: Literal[False] | Omit = omit, - x_sent_at: Union[str, datetime] | Omit = omit, x_stream_response: Literal["true", "false"] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. @@ -92,8 +90,6 @@ def act( stream_response: Whether to stream the response via SSE - x_sent_at: ISO timestamp when request was sent - x_stream_response: Whether to stream the response via SSE extra_headers: Send extra headers @@ -113,9 +109,8 @@ def act( *, input: session_act_params.Input, stream_response: Literal[True], - frame_id: str | Omit = omit, + frame_id: Optional[str] | Omit = omit, options: session_act_params.Options | Omit = omit, - x_sent_at: Union[str, datetime] | Omit = omit, x_stream_response: Literal["true", "false"] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. @@ -137,8 +132,6 @@ def act( frame_id: Target frame ID for the action - x_sent_at: ISO timestamp when request was sent - x_stream_response: Whether to stream the response via SSE extra_headers: Send extra headers @@ -158,9 +151,8 @@ def act( *, input: session_act_params.Input, stream_response: bool, - frame_id: str | Omit = omit, + frame_id: Optional[str] | Omit = omit, options: session_act_params.Options | Omit = omit, - x_sent_at: Union[str, datetime] | Omit = omit, x_stream_response: Literal["true", "false"] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. @@ -182,8 +174,6 @@ def act( frame_id: Target frame ID for the action - x_sent_at: ISO timestamp when request was sent - x_stream_response: Whether to stream the response via SSE extra_headers: Send extra headers @@ -202,10 +192,9 @@ def act( id: str, *, input: session_act_params.Input, - frame_id: str | Omit = omit, + frame_id: Optional[str] | Omit = omit, options: session_act_params.Options | Omit = omit, stream_response: Literal[False] | Literal[True] | Omit = omit, - x_sent_at: Union[str, datetime] | Omit = omit, x_stream_response: Literal["true", "false"] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. @@ -218,10 +207,7 @@ def act( raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") extra_headers = { **strip_not_given( - { - "x-sent-at": x_sent_at.isoformat() if is_given(x_sent_at) else not_given, - "x-stream-response": str(x_stream_response) if is_given(x_stream_response) else not_given, - } + {"x-stream-response": str(x_stream_response) if is_given(x_stream_response) else not_given} ), **(extra_headers or {}), } @@ -251,7 +237,6 @@ def end( id: str, *, _force_body: object | Omit = omit, - x_sent_at: Union[str, datetime] | Omit = omit, x_stream_response: Literal["true", "false"] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. @@ -266,8 +251,6 @@ def end( Args: id: Unique session identifier - x_sent_at: ISO timestamp when request was sent - x_stream_response: Whether to stream the response via SSE extra_headers: Send extra headers @@ -282,10 +265,7 @@ def end( raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") extra_headers = { **strip_not_given( - { - "x-sent-at": x_sent_at.isoformat() if is_given(x_sent_at) else not_given, - "x-stream-response": str(x_stream_response) if is_given(x_stream_response) else not_given, - } + {"x-stream-response": str(x_stream_response) if is_given(x_stream_response) else not_given} ), **(extra_headers or {}), } @@ -305,9 +285,8 @@ def execute( *, agent_config: session_execute_params.AgentConfig, execute_options: session_execute_params.ExecuteOptions, - frame_id: str | Omit = omit, + frame_id: Optional[str] | Omit = omit, stream_response: Literal[False] | Omit = omit, - x_sent_at: Union[str, datetime] | Omit = omit, x_stream_response: Literal["true", "false"] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. @@ -326,8 +305,6 @@ def execute( stream_response: Whether to stream the response via SSE - x_sent_at: ISO timestamp when request was sent - x_stream_response: Whether to stream the response via SSE extra_headers: Send extra headers @@ -348,8 +325,7 @@ def execute( agent_config: session_execute_params.AgentConfig, execute_options: session_execute_params.ExecuteOptions, stream_response: Literal[True], - frame_id: str | Omit = omit, - x_sent_at: Union[str, datetime] | Omit = omit, + frame_id: Optional[str] | Omit = omit, x_stream_response: Literal["true", "false"] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. @@ -368,8 +344,6 @@ def execute( frame_id: Target frame ID for the agent - x_sent_at: ISO timestamp when request was sent - x_stream_response: Whether to stream the response via SSE extra_headers: Send extra headers @@ -390,8 +364,7 @@ def execute( agent_config: session_execute_params.AgentConfig, execute_options: session_execute_params.ExecuteOptions, stream_response: bool, - frame_id: str | Omit = omit, - x_sent_at: Union[str, datetime] | Omit = omit, + frame_id: Optional[str] | Omit = omit, x_stream_response: Literal["true", "false"] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. @@ -410,8 +383,6 @@ def execute( frame_id: Target frame ID for the agent - x_sent_at: ISO timestamp when request was sent - x_stream_response: Whether to stream the response via SSE extra_headers: Send extra headers @@ -431,9 +402,8 @@ def execute( *, agent_config: session_execute_params.AgentConfig, execute_options: session_execute_params.ExecuteOptions, - frame_id: str | Omit = omit, + frame_id: Optional[str] | Omit = omit, stream_response: Literal[False] | Literal[True] | Omit = omit, - x_sent_at: Union[str, datetime] | Omit = omit, x_stream_response: Literal["true", "false"] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. @@ -446,10 +416,7 @@ def execute( raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") extra_headers = { **strip_not_given( - { - "x-sent-at": x_sent_at.isoformat() if is_given(x_sent_at) else not_given, - "x-stream-response": str(x_stream_response) if is_given(x_stream_response) else not_given, - } + {"x-stream-response": str(x_stream_response) if is_given(x_stream_response) else not_given} ), **(extra_headers or {}), } @@ -479,12 +446,11 @@ def extract( self, id: str, *, - frame_id: str | Omit = omit, + frame_id: Optional[str] | Omit = omit, instruction: str | Omit = omit, options: session_extract_params.Options | Omit = omit, schema: Dict[str, object] | Omit = omit, stream_response: Literal[False] | Omit = omit, - x_sent_at: Union[str, datetime] | Omit = omit, x_stream_response: Literal["true", "false"] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. @@ -507,8 +473,6 @@ def extract( stream_response: Whether to stream the response via SSE - x_sent_at: ISO timestamp when request was sent - x_stream_response: Whether to stream the response via SSE extra_headers: Send extra headers @@ -527,11 +491,10 @@ def extract( id: str, *, stream_response: Literal[True], - frame_id: str | Omit = omit, + frame_id: Optional[str] | Omit = omit, instruction: str | Omit = omit, options: session_extract_params.Options | Omit = omit, schema: Dict[str, object] | Omit = omit, - x_sent_at: Union[str, datetime] | Omit = omit, x_stream_response: Literal["true", "false"] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. @@ -554,8 +517,6 @@ def extract( schema: JSON Schema defining the structure of data to extract - x_sent_at: ISO timestamp when request was sent - x_stream_response: Whether to stream the response via SSE extra_headers: Send extra headers @@ -574,11 +535,10 @@ def extract( id: str, *, stream_response: bool, - frame_id: str | Omit = omit, + frame_id: Optional[str] | Omit = omit, instruction: str | Omit = omit, options: session_extract_params.Options | Omit = omit, schema: Dict[str, object] | Omit = omit, - x_sent_at: Union[str, datetime] | Omit = omit, x_stream_response: Literal["true", "false"] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. @@ -601,8 +561,6 @@ def extract( schema: JSON Schema defining the structure of data to extract - x_sent_at: ISO timestamp when request was sent - x_stream_response: Whether to stream the response via SSE extra_headers: Send extra headers @@ -619,12 +577,11 @@ def extract( self, id: str, *, - frame_id: str | Omit = omit, + frame_id: Optional[str] | Omit = omit, instruction: str | Omit = omit, options: session_extract_params.Options | Omit = omit, schema: Dict[str, object] | Omit = omit, stream_response: Literal[False] | Literal[True] | Omit = omit, - x_sent_at: Union[str, datetime] | Omit = omit, x_stream_response: Literal["true", "false"] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. @@ -637,10 +594,7 @@ def extract( raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") extra_headers = { **strip_not_given( - { - "x-sent-at": x_sent_at.isoformat() if is_given(x_sent_at) else not_given, - "x-stream-response": str(x_stream_response) if is_given(x_stream_response) else not_given, - } + {"x-stream-response": str(x_stream_response) if is_given(x_stream_response) else not_given} ), **(extra_headers or {}), } @@ -671,10 +625,9 @@ def navigate( id: str, *, url: str, - frame_id: str | Omit = omit, + frame_id: Optional[str] | Omit = omit, options: session_navigate_params.Options | Omit = omit, stream_response: bool | Omit = omit, - x_sent_at: Union[str, datetime] | Omit = omit, x_stream_response: Literal["true", "false"] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. @@ -695,8 +648,6 @@ def navigate( stream_response: Whether to stream the response via SSE - x_sent_at: ISO timestamp when request was sent - x_stream_response: Whether to stream the response via SSE extra_headers: Send extra headers @@ -711,10 +662,7 @@ def navigate( raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") extra_headers = { **strip_not_given( - { - "x-sent-at": x_sent_at.isoformat() if is_given(x_sent_at) else not_given, - "x-stream-response": str(x_stream_response) if is_given(x_stream_response) else not_given, - } + {"x-stream-response": str(x_stream_response) if is_given(x_stream_response) else not_given} ), **(extra_headers or {}), } @@ -740,11 +688,10 @@ def observe( self, id: str, *, - frame_id: str | Omit = omit, + frame_id: Optional[str] | Omit = omit, instruction: str | Omit = omit, options: session_observe_params.Options | Omit = omit, stream_response: Literal[False] | Omit = omit, - x_sent_at: Union[str, datetime] | Omit = omit, x_stream_response: Literal["true", "false"] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. @@ -766,8 +713,6 @@ def observe( stream_response: Whether to stream the response via SSE - x_sent_at: ISO timestamp when request was sent - x_stream_response: Whether to stream the response via SSE extra_headers: Send extra headers @@ -786,10 +731,9 @@ def observe( id: str, *, stream_response: Literal[True], - frame_id: str | Omit = omit, + frame_id: Optional[str] | Omit = omit, instruction: str | Omit = omit, options: session_observe_params.Options | Omit = omit, - x_sent_at: Union[str, datetime] | Omit = omit, x_stream_response: Literal["true", "false"] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. @@ -811,8 +755,6 @@ def observe( instruction: Natural language instruction for what actions to find - x_sent_at: ISO timestamp when request was sent - x_stream_response: Whether to stream the response via SSE extra_headers: Send extra headers @@ -831,10 +773,9 @@ def observe( id: str, *, stream_response: bool, - frame_id: str | Omit = omit, + frame_id: Optional[str] | Omit = omit, instruction: str | Omit = omit, options: session_observe_params.Options | Omit = omit, - x_sent_at: Union[str, datetime] | Omit = omit, x_stream_response: Literal["true", "false"] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. @@ -856,8 +797,6 @@ def observe( instruction: Natural language instruction for what actions to find - x_sent_at: ISO timestamp when request was sent - x_stream_response: Whether to stream the response via SSE extra_headers: Send extra headers @@ -874,11 +813,10 @@ def observe( self, id: str, *, - frame_id: str | Omit = omit, + frame_id: Optional[str] | Omit = omit, instruction: str | Omit = omit, options: session_observe_params.Options | Omit = omit, stream_response: Literal[False] | Literal[True] | Omit = omit, - x_sent_at: Union[str, datetime] | Omit = omit, x_stream_response: Literal["true", "false"] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. @@ -891,10 +829,7 @@ def observe( raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") extra_headers = { **strip_not_given( - { - "x-sent-at": x_sent_at.isoformat() if is_given(x_sent_at) else not_given, - "x-stream-response": str(x_stream_response) if is_given(x_stream_response) else not_given, - } + {"x-stream-response": str(x_stream_response) if is_given(x_stream_response) else not_given} ), **(extra_headers or {}), } @@ -933,7 +868,6 @@ def start( system_prompt: str | Omit = omit, verbose: Literal[0, 1, 2] | Omit = omit, wait_for_captcha_solves: bool | Omit = omit, - x_sent_at: Union[str, datetime] | Omit = omit, x_stream_response: Literal["true", "false"] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. @@ -948,7 +882,9 @@ def start( session ID used for all subsequent operations. Args: - model_name: Model name to use for AI operations + model_name: Model name to use for AI operations. Always use the format 'provider/model-name' + (e.g., 'openai/gpt-4o', 'anthropic/claude-sonnet-4-5-20250929', + 'google/gemini-2.0-flash') act_timeout_ms: Timeout in ms for act operations (deprecated, v2 only) @@ -964,8 +900,6 @@ def start( wait_for_captcha_solves: Wait for captcha solves (deprecated, v2 only) - x_sent_at: ISO timestamp when request was sent - x_stream_response: Whether to stream the response via SSE extra_headers: Send extra headers @@ -978,10 +912,7 @@ def start( """ extra_headers = { **strip_not_given( - { - "x-sent-at": x_sent_at.isoformat() if is_given(x_sent_at) else not_given, - "x-stream-response": str(x_stream_response) if is_given(x_stream_response) else not_given, - } + {"x-stream-response": str(x_stream_response) if is_given(x_stream_response) else not_given} ), **(extra_headers or {}), } @@ -1036,10 +967,9 @@ async def act( id: str, *, input: session_act_params.Input, - frame_id: str | Omit = omit, + frame_id: Optional[str] | Omit = omit, options: session_act_params.Options | Omit = omit, stream_response: Literal[False] | Omit = omit, - x_sent_at: Union[str, datetime] | Omit = omit, x_stream_response: Literal["true", "false"] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. @@ -1061,8 +991,6 @@ async def act( stream_response: Whether to stream the response via SSE - x_sent_at: ISO timestamp when request was sent - x_stream_response: Whether to stream the response via SSE extra_headers: Send extra headers @@ -1082,9 +1010,8 @@ async def act( *, input: session_act_params.Input, stream_response: Literal[True], - frame_id: str | Omit = omit, + frame_id: Optional[str] | Omit = omit, options: session_act_params.Options | Omit = omit, - x_sent_at: Union[str, datetime] | Omit = omit, x_stream_response: Literal["true", "false"] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. @@ -1106,8 +1033,6 @@ async def act( frame_id: Target frame ID for the action - x_sent_at: ISO timestamp when request was sent - x_stream_response: Whether to stream the response via SSE extra_headers: Send extra headers @@ -1127,9 +1052,8 @@ async def act( *, input: session_act_params.Input, stream_response: bool, - frame_id: str | Omit = omit, + frame_id: Optional[str] | Omit = omit, options: session_act_params.Options | Omit = omit, - x_sent_at: Union[str, datetime] | Omit = omit, x_stream_response: Literal["true", "false"] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. @@ -1151,8 +1075,6 @@ async def act( frame_id: Target frame ID for the action - x_sent_at: ISO timestamp when request was sent - x_stream_response: Whether to stream the response via SSE extra_headers: Send extra headers @@ -1171,10 +1093,9 @@ async def act( id: str, *, input: session_act_params.Input, - frame_id: str | Omit = omit, + frame_id: Optional[str] | Omit = omit, options: session_act_params.Options | Omit = omit, stream_response: Literal[False] | Literal[True] | Omit = omit, - x_sent_at: Union[str, datetime] | Omit = omit, x_stream_response: Literal["true", "false"] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. @@ -1187,10 +1108,7 @@ async def act( raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") extra_headers = { **strip_not_given( - { - "x-sent-at": x_sent_at.isoformat() if is_given(x_sent_at) else not_given, - "x-stream-response": str(x_stream_response) if is_given(x_stream_response) else not_given, - } + {"x-stream-response": str(x_stream_response) if is_given(x_stream_response) else not_given} ), **(extra_headers or {}), } @@ -1220,7 +1138,6 @@ async def end( id: str, *, _force_body: object | Omit = omit, - x_sent_at: Union[str, datetime] | Omit = omit, x_stream_response: Literal["true", "false"] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. @@ -1235,8 +1152,6 @@ async def end( Args: id: Unique session identifier - x_sent_at: ISO timestamp when request was sent - x_stream_response: Whether to stream the response via SSE extra_headers: Send extra headers @@ -1251,10 +1166,7 @@ async def end( raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") extra_headers = { **strip_not_given( - { - "x-sent-at": x_sent_at.isoformat() if is_given(x_sent_at) else not_given, - "x-stream-response": str(x_stream_response) if is_given(x_stream_response) else not_given, - } + {"x-stream-response": str(x_stream_response) if is_given(x_stream_response) else not_given} ), **(extra_headers or {}), } @@ -1274,9 +1186,8 @@ async def execute( *, agent_config: session_execute_params.AgentConfig, execute_options: session_execute_params.ExecuteOptions, - frame_id: str | Omit = omit, + frame_id: Optional[str] | Omit = omit, stream_response: Literal[False] | Omit = omit, - x_sent_at: Union[str, datetime] | Omit = omit, x_stream_response: Literal["true", "false"] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. @@ -1295,8 +1206,6 @@ async def execute( stream_response: Whether to stream the response via SSE - x_sent_at: ISO timestamp when request was sent - x_stream_response: Whether to stream the response via SSE extra_headers: Send extra headers @@ -1317,8 +1226,7 @@ async def execute( agent_config: session_execute_params.AgentConfig, execute_options: session_execute_params.ExecuteOptions, stream_response: Literal[True], - frame_id: str | Omit = omit, - x_sent_at: Union[str, datetime] | Omit = omit, + frame_id: Optional[str] | Omit = omit, x_stream_response: Literal["true", "false"] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. @@ -1337,8 +1245,6 @@ async def execute( frame_id: Target frame ID for the agent - x_sent_at: ISO timestamp when request was sent - x_stream_response: Whether to stream the response via SSE extra_headers: Send extra headers @@ -1359,8 +1265,7 @@ async def execute( agent_config: session_execute_params.AgentConfig, execute_options: session_execute_params.ExecuteOptions, stream_response: bool, - frame_id: str | Omit = omit, - x_sent_at: Union[str, datetime] | Omit = omit, + frame_id: Optional[str] | Omit = omit, x_stream_response: Literal["true", "false"] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. @@ -1379,8 +1284,6 @@ async def execute( frame_id: Target frame ID for the agent - x_sent_at: ISO timestamp when request was sent - x_stream_response: Whether to stream the response via SSE extra_headers: Send extra headers @@ -1400,9 +1303,8 @@ async def execute( *, agent_config: session_execute_params.AgentConfig, execute_options: session_execute_params.ExecuteOptions, - frame_id: str | Omit = omit, + frame_id: Optional[str] | Omit = omit, stream_response: Literal[False] | Literal[True] | Omit = omit, - x_sent_at: Union[str, datetime] | Omit = omit, x_stream_response: Literal["true", "false"] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. @@ -1415,10 +1317,7 @@ async def execute( raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") extra_headers = { **strip_not_given( - { - "x-sent-at": x_sent_at.isoformat() if is_given(x_sent_at) else not_given, - "x-stream-response": str(x_stream_response) if is_given(x_stream_response) else not_given, - } + {"x-stream-response": str(x_stream_response) if is_given(x_stream_response) else not_given} ), **(extra_headers or {}), } @@ -1448,12 +1347,11 @@ async def extract( self, id: str, *, - frame_id: str | Omit = omit, + frame_id: Optional[str] | Omit = omit, instruction: str | Omit = omit, options: session_extract_params.Options | Omit = omit, schema: Dict[str, object] | Omit = omit, stream_response: Literal[False] | Omit = omit, - x_sent_at: Union[str, datetime] | Omit = omit, x_stream_response: Literal["true", "false"] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. @@ -1476,8 +1374,6 @@ async def extract( stream_response: Whether to stream the response via SSE - x_sent_at: ISO timestamp when request was sent - x_stream_response: Whether to stream the response via SSE extra_headers: Send extra headers @@ -1496,11 +1392,10 @@ async def extract( id: str, *, stream_response: Literal[True], - frame_id: str | Omit = omit, + frame_id: Optional[str] | Omit = omit, instruction: str | Omit = omit, options: session_extract_params.Options | Omit = omit, schema: Dict[str, object] | Omit = omit, - x_sent_at: Union[str, datetime] | Omit = omit, x_stream_response: Literal["true", "false"] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. @@ -1523,8 +1418,6 @@ async def extract( schema: JSON Schema defining the structure of data to extract - x_sent_at: ISO timestamp when request was sent - x_stream_response: Whether to stream the response via SSE extra_headers: Send extra headers @@ -1543,11 +1436,10 @@ async def extract( id: str, *, stream_response: bool, - frame_id: str | Omit = omit, + frame_id: Optional[str] | Omit = omit, instruction: str | Omit = omit, options: session_extract_params.Options | Omit = omit, schema: Dict[str, object] | Omit = omit, - x_sent_at: Union[str, datetime] | Omit = omit, x_stream_response: Literal["true", "false"] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. @@ -1570,8 +1462,6 @@ async def extract( schema: JSON Schema defining the structure of data to extract - x_sent_at: ISO timestamp when request was sent - x_stream_response: Whether to stream the response via SSE extra_headers: Send extra headers @@ -1588,12 +1478,11 @@ async def extract( self, id: str, *, - frame_id: str | Omit = omit, + frame_id: Optional[str] | Omit = omit, instruction: str | Omit = omit, options: session_extract_params.Options | Omit = omit, schema: Dict[str, object] | Omit = omit, stream_response: Literal[False] | Literal[True] | Omit = omit, - x_sent_at: Union[str, datetime] | Omit = omit, x_stream_response: Literal["true", "false"] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. @@ -1606,10 +1495,7 @@ async def extract( raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") extra_headers = { **strip_not_given( - { - "x-sent-at": x_sent_at.isoformat() if is_given(x_sent_at) else not_given, - "x-stream-response": str(x_stream_response) if is_given(x_stream_response) else not_given, - } + {"x-stream-response": str(x_stream_response) if is_given(x_stream_response) else not_given} ), **(extra_headers or {}), } @@ -1640,10 +1526,9 @@ async def navigate( id: str, *, url: str, - frame_id: str | Omit = omit, + frame_id: Optional[str] | Omit = omit, options: session_navigate_params.Options | Omit = omit, stream_response: bool | Omit = omit, - x_sent_at: Union[str, datetime] | Omit = omit, x_stream_response: Literal["true", "false"] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. @@ -1664,8 +1549,6 @@ async def navigate( stream_response: Whether to stream the response via SSE - x_sent_at: ISO timestamp when request was sent - x_stream_response: Whether to stream the response via SSE extra_headers: Send extra headers @@ -1680,10 +1563,7 @@ async def navigate( raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") extra_headers = { **strip_not_given( - { - "x-sent-at": x_sent_at.isoformat() if is_given(x_sent_at) else not_given, - "x-stream-response": str(x_stream_response) if is_given(x_stream_response) else not_given, - } + {"x-stream-response": str(x_stream_response) if is_given(x_stream_response) else not_given} ), **(extra_headers or {}), } @@ -1709,11 +1589,10 @@ async def observe( self, id: str, *, - frame_id: str | Omit = omit, + frame_id: Optional[str] | Omit = omit, instruction: str | Omit = omit, options: session_observe_params.Options | Omit = omit, stream_response: Literal[False] | Omit = omit, - x_sent_at: Union[str, datetime] | Omit = omit, x_stream_response: Literal["true", "false"] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. @@ -1735,8 +1614,6 @@ async def observe( stream_response: Whether to stream the response via SSE - x_sent_at: ISO timestamp when request was sent - x_stream_response: Whether to stream the response via SSE extra_headers: Send extra headers @@ -1755,10 +1632,9 @@ async def observe( id: str, *, stream_response: Literal[True], - frame_id: str | Omit = omit, + frame_id: Optional[str] | Omit = omit, instruction: str | Omit = omit, options: session_observe_params.Options | Omit = omit, - x_sent_at: Union[str, datetime] | Omit = omit, x_stream_response: Literal["true", "false"] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. @@ -1780,8 +1656,6 @@ async def observe( instruction: Natural language instruction for what actions to find - x_sent_at: ISO timestamp when request was sent - x_stream_response: Whether to stream the response via SSE extra_headers: Send extra headers @@ -1800,10 +1674,9 @@ async def observe( id: str, *, stream_response: bool, - frame_id: str | Omit = omit, + frame_id: Optional[str] | Omit = omit, instruction: str | Omit = omit, options: session_observe_params.Options | Omit = omit, - x_sent_at: Union[str, datetime] | Omit = omit, x_stream_response: Literal["true", "false"] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. @@ -1825,8 +1698,6 @@ async def observe( instruction: Natural language instruction for what actions to find - x_sent_at: ISO timestamp when request was sent - x_stream_response: Whether to stream the response via SSE extra_headers: Send extra headers @@ -1843,11 +1714,10 @@ async def observe( self, id: str, *, - frame_id: str | Omit = omit, + frame_id: Optional[str] | Omit = omit, instruction: str | Omit = omit, options: session_observe_params.Options | Omit = omit, stream_response: Literal[False] | Literal[True] | Omit = omit, - x_sent_at: Union[str, datetime] | Omit = omit, x_stream_response: Literal["true", "false"] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. @@ -1860,10 +1730,7 @@ async def observe( raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") extra_headers = { **strip_not_given( - { - "x-sent-at": x_sent_at.isoformat() if is_given(x_sent_at) else not_given, - "x-stream-response": str(x_stream_response) if is_given(x_stream_response) else not_given, - } + {"x-stream-response": str(x_stream_response) if is_given(x_stream_response) else not_given} ), **(extra_headers or {}), } @@ -1902,7 +1769,6 @@ async def start( system_prompt: str | Omit = omit, verbose: Literal[0, 1, 2] | Omit = omit, wait_for_captcha_solves: bool | Omit = omit, - x_sent_at: Union[str, datetime] | Omit = omit, x_stream_response: Literal["true", "false"] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. @@ -1917,7 +1783,9 @@ async def start( session ID used for all subsequent operations. Args: - model_name: Model name to use for AI operations + model_name: Model name to use for AI operations. Always use the format 'provider/model-name' + (e.g., 'openai/gpt-4o', 'anthropic/claude-sonnet-4-5-20250929', + 'google/gemini-2.0-flash') act_timeout_ms: Timeout in ms for act operations (deprecated, v2 only) @@ -1933,8 +1801,6 @@ async def start( wait_for_captcha_solves: Wait for captcha solves (deprecated, v2 only) - x_sent_at: ISO timestamp when request was sent - x_stream_response: Whether to stream the response via SSE extra_headers: Send extra headers @@ -1947,10 +1813,7 @@ async def start( """ extra_headers = { **strip_not_given( - { - "x-sent-at": x_sent_at.isoformat() if is_given(x_sent_at) else not_given, - "x-stream-response": str(x_stream_response) if is_given(x_stream_response) else not_given, - } + {"x-stream-response": str(x_stream_response) if is_given(x_stream_response) else not_given} ), **(extra_headers or {}), } diff --git a/src/stagehand/types/model_config_param.py b/src/stagehand/types/model_config_param.py index f633db12..22e05a96 100644 --- a/src/stagehand/types/model_config_param.py +++ b/src/stagehand/types/model_config_param.py @@ -12,7 +12,11 @@ class ModelConfigObject(TypedDict, total=False): model_name: Required[Annotated[str, PropertyInfo(alias="modelName")]] - """Model name string (e.g., 'openai/gpt-5-nano', 'anthropic/claude-4.5-opus')""" + """Model name string with provider prefix. + + Always use the format 'provider/model-name' (e.g., 'openai/gpt-4o', + 'anthropic/claude-sonnet-4-5-20250929', 'google/gemini-2.0-flash') + """ api_key: Annotated[str, PropertyInfo(alias="apiKey")] """API key for the model provider""" diff --git a/src/stagehand/types/session_act_params.py b/src/stagehand/types/session_act_params.py index 605019b8..1a188ef0 100644 --- a/src/stagehand/types/session_act_params.py +++ b/src/stagehand/types/session_act_params.py @@ -2,8 +2,7 @@ from __future__ import annotations -from typing import Dict, Union -from datetime import datetime +from typing import Dict, Union, Optional from typing_extensions import Literal, Required, Annotated, TypeAlias, TypedDict from .._utils import PropertyInfo @@ -17,14 +16,11 @@ class SessionActParamsBase(TypedDict, total=False): input: Required[Input] """Natural language instruction or Action object""" - frame_id: Annotated[str, PropertyInfo(alias="frameId")] + frame_id: Annotated[Optional[str], PropertyInfo(alias="frameId")] """Target frame ID for the action""" options: Options - x_sent_at: Annotated[Union[str, datetime], PropertyInfo(alias="x-sent-at", format="iso8601")] - """ISO timestamp when request was sent""" - x_stream_response: Annotated[Literal["true", "false"], PropertyInfo(alias="x-stream-response")] """Whether to stream the response via SSE""" @@ -34,9 +30,10 @@ class SessionActParamsBase(TypedDict, total=False): class Options(TypedDict, total=False): model: ModelConfigParam - """ - Model name string with provider prefix (e.g., 'openai/gpt-5-nano', - 'anthropic/claude-4.5-opus') + """Model name string with provider prefix. + + Always use the format 'provider/model-name' (e.g., 'openai/gpt-4o', + 'anthropic/claude-sonnet-4-5-20250929', 'google/gemini-2.0-flash') """ timeout: float diff --git a/src/stagehand/types/session_end_params.py b/src/stagehand/types/session_end_params.py index 1998cd27..dd495b0f 100644 --- a/src/stagehand/types/session_end_params.py +++ b/src/stagehand/types/session_end_params.py @@ -2,8 +2,6 @@ from __future__ import annotations -from typing import Union -from datetime import datetime from typing_extensions import Literal, Annotated, TypedDict from .._utils import PropertyInfo @@ -14,8 +12,5 @@ class SessionEndParams(TypedDict, total=False): _force_body: Annotated[object, PropertyInfo(alias="_forceBody")] - x_sent_at: Annotated[Union[str, datetime], PropertyInfo(alias="x-sent-at", format="iso8601")] - """ISO timestamp when request was sent""" - x_stream_response: Annotated[Literal["true", "false"], PropertyInfo(alias="x-stream-response")] """Whether to stream the response via SSE""" diff --git a/src/stagehand/types/session_execute_params.py b/src/stagehand/types/session_execute_params.py index eb8678ff..5b390550 100644 --- a/src/stagehand/types/session_execute_params.py +++ b/src/stagehand/types/session_execute_params.py @@ -2,8 +2,7 @@ from __future__ import annotations -from typing import Union -from datetime import datetime +from typing import Union, Optional from typing_extensions import Literal, Required, Annotated, TypedDict from .._utils import PropertyInfo @@ -23,12 +22,9 @@ class SessionExecuteParamsBase(TypedDict, total=False): execute_options: Required[Annotated[ExecuteOptions, PropertyInfo(alias="executeOptions")]] - frame_id: Annotated[str, PropertyInfo(alias="frameId")] + frame_id: Annotated[Optional[str], PropertyInfo(alias="frameId")] """Target frame ID for the agent""" - x_sent_at: Annotated[Union[str, datetime], PropertyInfo(alias="x-sent-at", format="iso8601")] - """ISO timestamp when request was sent""" - x_stream_response: Annotated[Literal["true", "false"], PropertyInfo(alias="x-stream-response")] """Whether to stream the response via SSE""" @@ -38,9 +34,10 @@ class AgentConfig(TypedDict, total=False): """Enable Computer Use Agent mode""" model: ModelConfigParam - """ - Model name string with provider prefix (e.g., 'openai/gpt-5-nano', - 'anthropic/claude-4.5-opus') + """Model name string with provider prefix. + + Always use the format 'provider/model-name' (e.g., 'openai/gpt-4o', + 'anthropic/claude-sonnet-4-5-20250929', 'google/gemini-2.0-flash') """ provider: Literal["openai", "anthropic", "google", "microsoft"] diff --git a/src/stagehand/types/session_extract_params.py b/src/stagehand/types/session_extract_params.py index c763cbb5..019a3fc4 100644 --- a/src/stagehand/types/session_extract_params.py +++ b/src/stagehand/types/session_extract_params.py @@ -2,8 +2,7 @@ from __future__ import annotations -from typing import Dict, Union -from datetime import datetime +from typing import Dict, Union, Optional from typing_extensions import Literal, Required, Annotated, TypedDict from .._utils import PropertyInfo @@ -13,7 +12,7 @@ class SessionExtractParamsBase(TypedDict, total=False): - frame_id: Annotated[str, PropertyInfo(alias="frameId")] + frame_id: Annotated[Optional[str], PropertyInfo(alias="frameId")] """Target frame ID for the extraction""" instruction: str @@ -24,18 +23,16 @@ class SessionExtractParamsBase(TypedDict, total=False): schema: Dict[str, object] """JSON Schema defining the structure of data to extract""" - x_sent_at: Annotated[Union[str, datetime], PropertyInfo(alias="x-sent-at", format="iso8601")] - """ISO timestamp when request was sent""" - x_stream_response: Annotated[Literal["true", "false"], PropertyInfo(alias="x-stream-response")] """Whether to stream the response via SSE""" class Options(TypedDict, total=False): model: ModelConfigParam - """ - Model name string with provider prefix (e.g., 'openai/gpt-5-nano', - 'anthropic/claude-4.5-opus') + """Model name string with provider prefix. + + Always use the format 'provider/model-name' (e.g., 'openai/gpt-4o', + 'anthropic/claude-sonnet-4-5-20250929', 'google/gemini-2.0-flash') """ selector: str diff --git a/src/stagehand/types/session_navigate_params.py b/src/stagehand/types/session_navigate_params.py index 39bb2b2e..e93d0ec4 100644 --- a/src/stagehand/types/session_navigate_params.py +++ b/src/stagehand/types/session_navigate_params.py @@ -2,8 +2,7 @@ from __future__ import annotations -from typing import Union -from datetime import datetime +from typing import Optional from typing_extensions import Literal, Required, Annotated, TypedDict from .._utils import PropertyInfo @@ -15,7 +14,7 @@ class SessionNavigateParams(TypedDict, total=False): url: Required[str] """URL to navigate to""" - frame_id: Annotated[str, PropertyInfo(alias="frameId")] + frame_id: Annotated[Optional[str], PropertyInfo(alias="frameId")] """Target frame ID for the navigation""" options: Options @@ -23,9 +22,6 @@ class SessionNavigateParams(TypedDict, total=False): stream_response: Annotated[bool, PropertyInfo(alias="streamResponse")] """Whether to stream the response via SSE""" - x_sent_at: Annotated[Union[str, datetime], PropertyInfo(alias="x-sent-at", format="iso8601")] - """ISO timestamp when request was sent""" - x_stream_response: Annotated[Literal["true", "false"], PropertyInfo(alias="x-stream-response")] """Whether to stream the response via SSE""" diff --git a/src/stagehand/types/session_observe_params.py b/src/stagehand/types/session_observe_params.py index 7cad4c85..349104ec 100644 --- a/src/stagehand/types/session_observe_params.py +++ b/src/stagehand/types/session_observe_params.py @@ -2,8 +2,7 @@ from __future__ import annotations -from typing import Union -from datetime import datetime +from typing import Union, Optional from typing_extensions import Literal, Required, Annotated, TypedDict from .._utils import PropertyInfo @@ -13,7 +12,7 @@ class SessionObserveParamsBase(TypedDict, total=False): - frame_id: Annotated[str, PropertyInfo(alias="frameId")] + frame_id: Annotated[Optional[str], PropertyInfo(alias="frameId")] """Target frame ID for the observation""" instruction: str @@ -21,18 +20,16 @@ class SessionObserveParamsBase(TypedDict, total=False): options: Options - x_sent_at: Annotated[Union[str, datetime], PropertyInfo(alias="x-sent-at", format="iso8601")] - """ISO timestamp when request was sent""" - x_stream_response: Annotated[Literal["true", "false"], PropertyInfo(alias="x-stream-response")] """Whether to stream the response via SSE""" class Options(TypedDict, total=False): model: ModelConfigParam - """ - Model name string with provider prefix (e.g., 'openai/gpt-5-nano', - 'anthropic/claude-4.5-opus') + """Model name string with provider prefix. + + Always use the format 'provider/model-name' (e.g., 'openai/gpt-4o', + 'anthropic/claude-sonnet-4-5-20250929', 'google/gemini-2.0-flash') """ selector: str diff --git a/src/stagehand/types/session_start_params.py b/src/stagehand/types/session_start_params.py index 03735673..1b05af5d 100644 --- a/src/stagehand/types/session_start_params.py +++ b/src/stagehand/types/session_start_params.py @@ -3,7 +3,6 @@ from __future__ import annotations from typing import Dict, List, Union, Iterable -from datetime import datetime from typing_extensions import Literal, Required, Annotated, TypeAlias, TypedDict from .._types import SequenceNotStr @@ -30,7 +29,11 @@ class SessionStartParams(TypedDict, total=False): model_name: Required[Annotated[str, PropertyInfo(alias="modelName")]] - """Model name to use for AI operations""" + """Model name to use for AI operations. + + Always use the format 'provider/model-name' (e.g., 'openai/gpt-4o', + 'anthropic/claude-sonnet-4-5-20250929', 'google/gemini-2.0-flash') + """ act_timeout_ms: Annotated[float, PropertyInfo(alias="actTimeoutMs")] """Timeout in ms for act operations (deprecated, v2 only)""" @@ -61,9 +64,6 @@ class SessionStartParams(TypedDict, total=False): wait_for_captcha_solves: Annotated[bool, PropertyInfo(alias="waitForCaptchaSolves")] """Wait for captcha solves (deprecated, v2 only)""" - x_sent_at: Annotated[Union[str, datetime], PropertyInfo(alias="x-sent-at", format="iso8601")] - """ISO timestamp when request was sent""" - x_stream_response: Annotated[Literal["true", "false"], PropertyInfo(alias="x-stream-response")] """Whether to stream the response via SSE""" diff --git a/tests/api_resources/test_sessions.py b/tests/api_resources/test_sessions.py index 473a917d..e2c13c68 100644 --- a/tests/api_resources/test_sessions.py +++ b/tests/api_resources/test_sessions.py @@ -18,7 +18,6 @@ SessionObserveResponse, SessionNavigateResponse, ) -from stagehand._utils import parse_datetime base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") @@ -43,12 +42,11 @@ def test_method_act_with_all_params_overload_1(self, client: Stagehand) -> None: input="Click the login button", frame_id="frameId", options={ - "model": "openai/gpt-5-nano", + "model": "openai/gpt-4o", "timeout": 30000, "variables": {"username": "john_doe"}, }, stream_response=False, - x_sent_at=parse_datetime("2025-01-15T10:30:00Z"), x_stream_response="true", ) assert_matches_type(SessionActResponse, session, path=["response"]) @@ -109,11 +107,10 @@ def test_method_act_with_all_params_overload_2(self, client: Stagehand) -> None: stream_response=True, frame_id="frameId", options={ - "model": "openai/gpt-5-nano", + "model": "openai/gpt-4o", "timeout": 30000, "variables": {"username": "john_doe"}, }, - x_sent_at=parse_datetime("2025-01-15T10:30:00Z"), x_stream_response="true", ) session_stream.response.close() @@ -171,7 +168,6 @@ def test_method_end_with_all_params(self, client: Stagehand) -> None: session = client.sessions.end( id="c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123", _force_body={}, - x_sent_at=parse_datetime("2025-01-15T10:30:00Z"), x_stream_response="true", ) assert_matches_type(SessionEndResponse, session, path=["response"]) @@ -229,7 +225,7 @@ def test_method_execute_with_all_params_overload_1(self, client: Stagehand) -> N id="c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123", agent_config={ "cua": True, - "model": "openai/gpt-5-nano", + "model": "openai/gpt-4o", "provider": "openai", "system_prompt": "systemPrompt", }, @@ -240,7 +236,6 @@ def test_method_execute_with_all_params_overload_1(self, client: Stagehand) -> N }, frame_id="frameId", stream_response=False, - x_sent_at=parse_datetime("2025-01-15T10:30:00Z"), x_stream_response="true", ) assert_matches_type(SessionExecuteResponse, session, path=["response"]) @@ -311,7 +306,7 @@ def test_method_execute_with_all_params_overload_2(self, client: Stagehand) -> N id="c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123", agent_config={ "cua": True, - "model": "openai/gpt-5-nano", + "model": "openai/gpt-4o", "provider": "openai", "system_prompt": "systemPrompt", }, @@ -322,7 +317,6 @@ def test_method_execute_with_all_params_overload_2(self, client: Stagehand) -> N }, stream_response=True, frame_id="frameId", - x_sent_at=parse_datetime("2025-01-15T10:30:00Z"), x_stream_response="true", ) session_stream.response.close() @@ -391,13 +385,12 @@ def test_method_extract_with_all_params_overload_1(self, client: Stagehand) -> N frame_id="frameId", instruction="Extract all product names and prices from the page", options={ - "model": "openai/gpt-5-nano", + "model": "openai/gpt-4o", "selector": "#main-content", "timeout": 30000, }, schema={"foo": "bar"}, stream_response=False, - x_sent_at=parse_datetime("2025-01-15T10:30:00Z"), x_stream_response="true", ) assert_matches_type(SessionExtractResponse, session, path=["response"]) @@ -454,12 +447,11 @@ def test_method_extract_with_all_params_overload_2(self, client: Stagehand) -> N frame_id="frameId", instruction="Extract all product names and prices from the page", options={ - "model": "openai/gpt-5-nano", + "model": "openai/gpt-4o", "selector": "#main-content", "timeout": 30000, }, schema={"foo": "bar"}, - x_sent_at=parse_datetime("2025-01-15T10:30:00Z"), x_stream_response="true", ) session_stream.response.close() @@ -522,7 +514,6 @@ def test_method_navigate_with_all_params(self, client: Stagehand) -> None: "wait_until": "networkidle", }, stream_response=True, - x_sent_at=parse_datetime("2025-01-15T10:30:00Z"), x_stream_response="true", ) assert_matches_type(SessionNavigateResponse, session, path=["response"]) @@ -580,12 +571,11 @@ def test_method_observe_with_all_params_overload_1(self, client: Stagehand) -> N frame_id="frameId", instruction="Find all clickable navigation links", options={ - "model": "openai/gpt-5-nano", + "model": "openai/gpt-4o", "selector": "nav", "timeout": 30000, }, stream_response=False, - x_sent_at=parse_datetime("2025-01-15T10:30:00Z"), x_stream_response="true", ) assert_matches_type(SessionObserveResponse, session, path=["response"]) @@ -642,11 +632,10 @@ def test_method_observe_with_all_params_overload_2(self, client: Stagehand) -> N frame_id="frameId", instruction="Find all clickable navigation links", options={ - "model": "openai/gpt-5-nano", + "model": "openai/gpt-4o", "selector": "nav", "timeout": 30000, }, - x_sent_at=parse_datetime("2025-01-15T10:30:00Z"), x_stream_response="true", ) session_stream.response.close() @@ -778,7 +767,6 @@ def test_method_start_with_all_params(self, client: Stagehand) -> None: system_prompt="systemPrompt", verbose=1, wait_for_captcha_solves=True, - x_sent_at=parse_datetime("2025-01-15T10:30:00Z"), x_stream_response="true", ) assert_matches_type(SessionStartResponse, session, path=["response"]) @@ -832,12 +820,11 @@ async def test_method_act_with_all_params_overload_1(self, async_client: AsyncSt input="Click the login button", frame_id="frameId", options={ - "model": "openai/gpt-5-nano", + "model": "openai/gpt-4o", "timeout": 30000, "variables": {"username": "john_doe"}, }, stream_response=False, - x_sent_at=parse_datetime("2025-01-15T10:30:00Z"), x_stream_response="true", ) assert_matches_type(SessionActResponse, session, path=["response"]) @@ -898,11 +885,10 @@ async def test_method_act_with_all_params_overload_2(self, async_client: AsyncSt stream_response=True, frame_id="frameId", options={ - "model": "openai/gpt-5-nano", + "model": "openai/gpt-4o", "timeout": 30000, "variables": {"username": "john_doe"}, }, - x_sent_at=parse_datetime("2025-01-15T10:30:00Z"), x_stream_response="true", ) await session_stream.response.aclose() @@ -960,7 +946,6 @@ async def test_method_end_with_all_params(self, async_client: AsyncStagehand) -> session = await async_client.sessions.end( id="c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123", _force_body={}, - x_sent_at=parse_datetime("2025-01-15T10:30:00Z"), x_stream_response="true", ) assert_matches_type(SessionEndResponse, session, path=["response"]) @@ -1018,7 +1003,7 @@ async def test_method_execute_with_all_params_overload_1(self, async_client: Asy id="c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123", agent_config={ "cua": True, - "model": "openai/gpt-5-nano", + "model": "openai/gpt-4o", "provider": "openai", "system_prompt": "systemPrompt", }, @@ -1029,7 +1014,6 @@ async def test_method_execute_with_all_params_overload_1(self, async_client: Asy }, frame_id="frameId", stream_response=False, - x_sent_at=parse_datetime("2025-01-15T10:30:00Z"), x_stream_response="true", ) assert_matches_type(SessionExecuteResponse, session, path=["response"]) @@ -1100,7 +1084,7 @@ async def test_method_execute_with_all_params_overload_2(self, async_client: Asy id="c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123", agent_config={ "cua": True, - "model": "openai/gpt-5-nano", + "model": "openai/gpt-4o", "provider": "openai", "system_prompt": "systemPrompt", }, @@ -1111,7 +1095,6 @@ async def test_method_execute_with_all_params_overload_2(self, async_client: Asy }, stream_response=True, frame_id="frameId", - x_sent_at=parse_datetime("2025-01-15T10:30:00Z"), x_stream_response="true", ) await session_stream.response.aclose() @@ -1180,13 +1163,12 @@ async def test_method_extract_with_all_params_overload_1(self, async_client: Asy frame_id="frameId", instruction="Extract all product names and prices from the page", options={ - "model": "openai/gpt-5-nano", + "model": "openai/gpt-4o", "selector": "#main-content", "timeout": 30000, }, schema={"foo": "bar"}, stream_response=False, - x_sent_at=parse_datetime("2025-01-15T10:30:00Z"), x_stream_response="true", ) assert_matches_type(SessionExtractResponse, session, path=["response"]) @@ -1243,12 +1225,11 @@ async def test_method_extract_with_all_params_overload_2(self, async_client: Asy frame_id="frameId", instruction="Extract all product names and prices from the page", options={ - "model": "openai/gpt-5-nano", + "model": "openai/gpt-4o", "selector": "#main-content", "timeout": 30000, }, schema={"foo": "bar"}, - x_sent_at=parse_datetime("2025-01-15T10:30:00Z"), x_stream_response="true", ) await session_stream.response.aclose() @@ -1311,7 +1292,6 @@ async def test_method_navigate_with_all_params(self, async_client: AsyncStagehan "wait_until": "networkidle", }, stream_response=True, - x_sent_at=parse_datetime("2025-01-15T10:30:00Z"), x_stream_response="true", ) assert_matches_type(SessionNavigateResponse, session, path=["response"]) @@ -1369,12 +1349,11 @@ async def test_method_observe_with_all_params_overload_1(self, async_client: Asy frame_id="frameId", instruction="Find all clickable navigation links", options={ - "model": "openai/gpt-5-nano", + "model": "openai/gpt-4o", "selector": "nav", "timeout": 30000, }, stream_response=False, - x_sent_at=parse_datetime("2025-01-15T10:30:00Z"), x_stream_response="true", ) assert_matches_type(SessionObserveResponse, session, path=["response"]) @@ -1431,11 +1410,10 @@ async def test_method_observe_with_all_params_overload_2(self, async_client: Asy frame_id="frameId", instruction="Find all clickable navigation links", options={ - "model": "openai/gpt-5-nano", + "model": "openai/gpt-4o", "selector": "nav", "timeout": 30000, }, - x_sent_at=parse_datetime("2025-01-15T10:30:00Z"), x_stream_response="true", ) await session_stream.response.aclose() @@ -1567,7 +1545,6 @@ async def test_method_start_with_all_params(self, async_client: AsyncStagehand) system_prompt="systemPrompt", verbose=1, wait_for_captcha_solves=True, - x_sent_at=parse_datetime("2025-01-15T10:30:00Z"), x_stream_response="true", ) assert_matches_type(SessionStartResponse, session, path=["response"]) From e8d79593740b0c764c0ae9752b9849c6ebdbf1be Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 16 Jan 2026 00:42:13 +0000 Subject: [PATCH 88/88] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index ee1de314..71adca43 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 7 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fstagehand-07032b695bc66ecd76328d936b41e01cfad508a870928c09c185f5faa5ea66ab.yml openapi_spec_hash: fca4b895ce36ad547fb015c3dd38821f -config_hash: d4df55e4b30aac2d8d0982be97f837c4 +config_hash: bf22187c626f0401180a332e3f3f6d8c