From aedee98560c6c13e993c6b7d368c37592a5838bc Mon Sep 17 00:00:00 2001 From: stainless-bot Date: Thu, 24 Oct 2024 22:17:44 +0000 Subject: [PATCH 001/330] initial commit --- .devcontainer/Dockerfile | 9 + .devcontainer/devcontainer.json | 40 + .github/workflows/ci.yml | 53 + .gitignore | 16 + .python-version | 1 + .stats.yml | 2 + Brewfile | 2 + CONTRIBUTING.md | 129 ++ LICENSE | 201 ++ README.md | 339 ++- SECURITY.md | 27 + api.md | 99 + bin/publish-pypi | 9 + examples/.keep | 4 + mypy.ini | 47 + noxfile.py | 9 + pyproject.toml | 208 ++ requirements-dev.lock | 105 + requirements.lock | 45 + scripts/bootstrap | 19 + scripts/format | 8 + scripts/lint | 12 + scripts/mock | 41 + scripts/test | 59 + scripts/utils/ruffen-docs.py | 167 ++ src/browserbase/__init__.py | 93 + src/browserbase/_base_client.py | 2041 +++++++++++++++++ src/browserbase/_client.py | 430 ++++ src/browserbase/_compat.py | 219 ++ src/browserbase/_constants.py | 14 + src/browserbase/_exceptions.py | 108 + src/browserbase/_files.py | 123 + src/browserbase/_models.py | 785 +++++++ src/browserbase/_qs.py | 150 ++ src/browserbase/_resource.py | 43 + src/browserbase/_response.py | 826 +++++++ src/browserbase/_streaming.py | 333 +++ src/browserbase/_types.py | 217 ++ src/browserbase/_utils/__init__.py | 55 + src/browserbase/_utils/_logs.py | 25 + src/browserbase/_utils/_proxy.py | 62 + src/browserbase/_utils/_reflection.py | 42 + src/browserbase/_utils/_streams.py | 12 + src/browserbase/_utils/_sync.py | 81 + src/browserbase/_utils/_transform.py | 382 +++ src/browserbase/_utils/_typing.py | 120 + src/browserbase/_utils/_utils.py | 397 ++++ src/browserbase/_version.py | 4 + src/browserbase/lib/.keep | 4 + src/browserbase/py.typed | 0 src/browserbase/resources/__init__.py | 61 + src/browserbase/resources/contexts.py | 332 +++ src/browserbase/resources/extensions.py | 342 +++ src/browserbase/resources/projects.py | 293 +++ .../resources/sessions/__init__.py | 75 + .../resources/sessions/downloads.py | 172 ++ src/browserbase/resources/sessions/logs.py | 163 ++ .../resources/sessions/recording.py | 163 ++ .../resources/sessions/sessions.py | 709 ++++++ src/browserbase/resources/sessions/uploads.py | 190 ++ src/browserbase/types/__init__.py | 19 + src/browserbase/types/context.py | 17 + .../types/context_create_params.py | 17 + .../types/context_create_response.py | 30 + .../types/context_update_response.py | 30 + src/browserbase/types/extension.py | 17 + .../types/extension_create_params.py | 13 + src/browserbase/types/project.py | 17 + .../types/project_list_response.py | 10 + src/browserbase/types/project_usage.py | 14 + src/browserbase/types/session.py | 17 + .../types/session_create_params.py | 186 ++ src/browserbase/types/session_list_params.py | 11 + .../types/session_list_response.py | 10 + src/browserbase/types/session_live_urls.py | 33 + .../types/session_update_params.py | 23 + src/browserbase/types/sessions/__init__.py | 10 + .../types/sessions/log_list_response.py | 10 + .../sessions/recording_retrieve_response.py | 10 + src/browserbase/types/sessions/session_log.py | 48 + .../types/sessions/session_recording.py | 26 + .../types/sessions/upload_create_params.py | 13 + .../types/sessions/upload_create_response.py | 10 + tests/__init__.py | 1 + tests/api_resources/__init__.py | 1 + tests/api_resources/sessions/__init__.py | 1 + .../api_resources/sessions/test_downloads.py | 128 ++ tests/api_resources/sessions/test_logs.py | 98 + .../api_resources/sessions/test_recording.py | 98 + tests/api_resources/sessions/test_uploads.py | 106 + tests/api_resources/test_contexts.py | 236 ++ tests/api_resources/test_extensions.py | 236 ++ tests/api_resources/test_projects.py | 224 ++ tests/api_resources/test_sessions.py | 474 ++++ tests/conftest.py | 49 + tests/sample_file.txt | 1 + tests/test_client.py | 1610 +++++++++++++ tests/test_deepcopy.py | 58 + tests/test_extract_files.py | 64 + tests/test_files.py | 51 + tests/test_models.py | 829 +++++++ tests/test_qs.py | 78 + tests/test_required_args.py | 111 + tests/test_response.py | 277 +++ tests/test_streaming.py | 252 ++ tests/test_transform.py | 410 ++++ tests/test_utils/test_proxy.py | 23 + tests/test_utils/test_typing.py | 73 + tests/utils.py | 155 ++ 109 files changed, 16951 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 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 mypy.ini 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 100644 src/browserbase/__init__.py create mode 100644 src/browserbase/_base_client.py create mode 100644 src/browserbase/_client.py create mode 100644 src/browserbase/_compat.py create mode 100644 src/browserbase/_constants.py create mode 100644 src/browserbase/_exceptions.py create mode 100644 src/browserbase/_files.py create mode 100644 src/browserbase/_models.py create mode 100644 src/browserbase/_qs.py create mode 100644 src/browserbase/_resource.py create mode 100644 src/browserbase/_response.py create mode 100644 src/browserbase/_streaming.py create mode 100644 src/browserbase/_types.py create mode 100644 src/browserbase/_utils/__init__.py create mode 100644 src/browserbase/_utils/_logs.py create mode 100644 src/browserbase/_utils/_proxy.py create mode 100644 src/browserbase/_utils/_reflection.py create mode 100644 src/browserbase/_utils/_streams.py create mode 100644 src/browserbase/_utils/_sync.py create mode 100644 src/browserbase/_utils/_transform.py create mode 100644 src/browserbase/_utils/_typing.py create mode 100644 src/browserbase/_utils/_utils.py create mode 100644 src/browserbase/_version.py create mode 100644 src/browserbase/lib/.keep create mode 100644 src/browserbase/py.typed create mode 100644 src/browserbase/resources/__init__.py create mode 100644 src/browserbase/resources/contexts.py create mode 100644 src/browserbase/resources/extensions.py create mode 100644 src/browserbase/resources/projects.py create mode 100644 src/browserbase/resources/sessions/__init__.py create mode 100644 src/browserbase/resources/sessions/downloads.py create mode 100644 src/browserbase/resources/sessions/logs.py create mode 100644 src/browserbase/resources/sessions/recording.py create mode 100644 src/browserbase/resources/sessions/sessions.py create mode 100644 src/browserbase/resources/sessions/uploads.py create mode 100644 src/browserbase/types/__init__.py create mode 100644 src/browserbase/types/context.py create mode 100644 src/browserbase/types/context_create_params.py create mode 100644 src/browserbase/types/context_create_response.py create mode 100644 src/browserbase/types/context_update_response.py create mode 100644 src/browserbase/types/extension.py create mode 100644 src/browserbase/types/extension_create_params.py create mode 100644 src/browserbase/types/project.py create mode 100644 src/browserbase/types/project_list_response.py create mode 100644 src/browserbase/types/project_usage.py create mode 100644 src/browserbase/types/session.py create mode 100644 src/browserbase/types/session_create_params.py create mode 100644 src/browserbase/types/session_list_params.py create mode 100644 src/browserbase/types/session_list_response.py create mode 100644 src/browserbase/types/session_live_urls.py create mode 100644 src/browserbase/types/session_update_params.py create mode 100644 src/browserbase/types/sessions/__init__.py create mode 100644 src/browserbase/types/sessions/log_list_response.py create mode 100644 src/browserbase/types/sessions/recording_retrieve_response.py create mode 100644 src/browserbase/types/sessions/session_log.py create mode 100644 src/browserbase/types/sessions/session_recording.py create mode 100644 src/browserbase/types/sessions/upload_create_params.py create mode 100644 src/browserbase/types/sessions/upload_create_response.py create mode 100644 tests/__init__.py create mode 100644 tests/api_resources/__init__.py create mode 100644 tests/api_resources/sessions/__init__.py create mode 100644 tests/api_resources/sessions/test_downloads.py create mode 100644 tests/api_resources/sessions/test_logs.py create mode 100644 tests/api_resources/sessions/test_recording.py create mode 100644 tests/api_resources/sessions/test_uploads.py create mode 100644 tests/api_resources/test_contexts.py create mode 100644 tests/api_resources/test_extensions.py create mode 100644 tests/api_resources/test_projects.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_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..ac9a2e75 --- /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.35.0" RYE_INSTALL_OPTION="--yes" bash +ENV PATH=/home/vscode/.rye/shims:$PATH + +RUN echo "[[ -d .venv ]] && source .venv/bin/activate" >> /home/vscode/.bashrc diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 00000000..bbeb30b1 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,40 @@ +// 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 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..40293964 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,53 @@ +name: CI +on: + push: + branches: + - main + pull_request: + branches: + - main + - next + +jobs: + lint: + name: lint + 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.35.0' + RYE_INSTALL_OPTION: '--yes' + + - name: Install dependencies + run: rye sync --all-features + + - name: Run lints + run: ./scripts/lint + test: + name: test + 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.35.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..87797408 --- /dev/null +++ b/.gitignore @@ -0,0 +1,16 @@ +.prism.log +.vscode +_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..e74d0eed --- /dev/null +++ b/.stats.yml @@ -0,0 +1,2 @@ +configured_endpoints: 18 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fbrowserbase-099e8b99a50c73a107fe278d9d286dca1cc4b26769aa223ea1bcf9924ba38467.yml 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..8f94874b --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,129 @@ +## 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 +$ rye shell +# or 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 +$ 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/browserbase/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/browserbase-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/browserbase-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..915e6f84 --- /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 2024 Browserbase + + 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 975414f7..dc64ac26 100644 --- a/README.md +++ b/README.md @@ -1 +1,338 @@ -# browserbase-python \ No newline at end of file +# Browserbase Python API library + +[![PyPI version](https://img.shields.io/pypi/v/browserbase.svg)](https://pypi.org/project/browserbase/) + +The Browserbase Python library provides convenient access to the Browserbase REST API from any Python 3.7+ +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.stainlessapi.com/). + +## Documentation + +The REST API documentation can be found on [docs.browserbase.com](https://docs.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/browserbase-python.git +``` + +> [!NOTE] +> Once this package is [published to PyPI](https://app.stainlessapi.com/docs/guides/publish), this will become: `pip install --pre browserbase` + +## Usage + +The full API of this library can be found in [api.md](api.md). + +```python +import os +from browserbase import Browserbase + +client = Browserbase( + # This is the default and can be omitted + api_key=os.environ.get("BROWSERBASE_API_KEY"), +) + +context = client.contexts.create( + project_id="projectId", +) +print(context.id) +``` + +While you can provide an `api_key` keyword argument, +we recommend using [python-dotenv](https://pypi.org/project/python-dotenv/) +to add `BROWSERBASE_API_KEY="My API Key"` to your `.env` file +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: + +```python +import os +import asyncio +from browserbase import AsyncBrowserbase + +client = AsyncBrowserbase( + # This is the default and can be omitted + api_key=os.environ.get("BROWSERBASE_API_KEY"), +) + + +async def main() -> None: + context = await client.contexts.create( + project_id="projectId", + ) + print(context.id) + + +asyncio.run(main()) +``` + +Functionality between the synchronous and asynchronous clients is otherwise identical. + +## 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`. + +## 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 `browserbase.APIConnectionError` is raised. + +When the API returns a non-success status code (that is, 4xx or 5xx +response), a subclass of `browserbase.APIStatusError` is raised, containing `status_code` and `response` properties. + +All errors inherit from `browserbase.APIError`. + +```python +import browserbase +from browserbase import Browserbase + +client = Browserbase() + +try: + client.contexts.create( + project_id="projectId", + ) +except browserbase.APIConnectionError as e: + print("The server could not be reached") + print(e.__cause__) # an underlying Exception, likely raised within httpx. +except browserbase.RateLimitError as e: + print("A 429 status code was received; we should back off a bit.") +except browserbase.APIStatusError as e: + print("Another non-200-range status code was received") + print(e.status_code) + print(e.response) +``` + +Error codes are as followed: + +| 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 browserbase import Browserbase + +# Configure the default for all requests: +client = Browserbase( + # default is 2 + max_retries=0, +) + +# Or, configure per-request: +client.with_options(max_retries=5).contexts.create( + project_id="projectId", +) +``` + +### 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/#fine-tuning-the-configuration) object: + +```python +from browserbase import Browserbase + +# Configure the default for all requests: +client = Browserbase( + # 20 seconds (default is 1 minute) + timeout=20.0, +) + +# More granular control: +client = Browserbase( + timeout=httpx.Timeout(60.0, read=5.0, write=10.0, connect=2.0), +) + +# Override per-request: +client.with_options(timeout=5.0).contexts.create( + project_id="projectId", +) +``` + +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 `BROWSERBASE_LOG` to `debug`. + +```shell +$ export BROWSERBASE_LOG=debug +``` + +### 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 browserbase import Browserbase + +client = Browserbase() +response = client.contexts.with_raw_response.create( + project_id="projectId", +) +print(response.headers.get('X-My-Header')) + +context = response.parse() # get the object that `contexts.create()` would have returned +print(context.id) +``` + +These methods return an [`APIResponse`](https://github.com/stainless-sdks/browserbase-python/tree/main/src/browserbase/_response.py) object. + +The async client returns an [`AsyncAPIResponse`](https://github.com/stainless-sdks/browserbase-python/tree/main/src/browserbase/_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.contexts.with_streaming_response.create( + project_id="projectId", +) 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) will be respected 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 +- Custom transports +- Additional [advanced](https://www.python-httpx.org/advanced/clients/) functionality + +```python +from browserbase import Browserbase, DefaultHttpxClient + +client = Browserbase( + # Or use the `BROWSERBASE_BASE_URL` env var + base_url="http://my.test.server.example.com:8083", + http_client=DefaultHttpxClient( + proxies="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. + +## 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/browserbase-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 browserbase +print(browserbase.__version__) +``` + +## Requirements + +Python 3.7 or higher. + +## Contributing + +See [the contributing documentation](./CONTRIBUTING.md). diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 00000000..06c5b6e8 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,27 @@ +# Security Policy + +## Reporting Security Issues + +This SDK is generated by [Stainless Software Inc](http://stainlessapi.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@stainlessapi.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 Browserbase please follow the respective company's security reporting guidelines. + +### Browserbase Terms and Policies + +Please contact dev-feedback@browserbase.com for any questions or concerns regarding security of our services. + +--- + +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..40594f6d --- /dev/null +++ b/api.md @@ -0,0 +1,99 @@ +# Contexts + +Types: + +```python +from browserbase.types import Context, ContextCreateResponse, ContextUpdateResponse +``` + +Methods: + +- client.contexts.create(\*\*params) -> ContextCreateResponse +- client.contexts.retrieve(id) -> Context +- client.contexts.update(id) -> ContextUpdateResponse + +# Extensions + +Types: + +```python +from browserbase.types import Extension +``` + +Methods: + +- client.extensions.create(\*\*params) -> Extension +- client.extensions.retrieve(id) -> Extension +- client.extensions.delete(id) -> None + +# Projects + +Types: + +```python +from browserbase.types import Project, ProjectUsage, ProjectListResponse +``` + +Methods: + +- client.projects.retrieve(id) -> Project +- client.projects.list() -> ProjectListResponse +- client.projects.usage(id) -> ProjectUsage + +# Sessions + +Types: + +```python +from browserbase.types import Session, SessionLiveURLs, SessionListResponse +``` + +Methods: + +- client.sessions.create(\*\*params) -> Session +- client.sessions.retrieve(id) -> Session +- client.sessions.update(id, \*\*params) -> Session +- client.sessions.list(\*\*params) -> SessionListResponse +- client.sessions.debug(id) -> SessionLiveURLs + +## Downloads + +Methods: + +- client.sessions.downloads.list(id) -> BinaryAPIResponse + +## Logs + +Types: + +```python +from browserbase.types.sessions import SessionLog, LogListResponse +``` + +Methods: + +- client.sessions.logs.list(id) -> LogListResponse + +## Recording + +Types: + +```python +from browserbase.types.sessions import SessionRecording, RecordingRetrieveResponse +``` + +Methods: + +- client.sessions.recording.retrieve(id) -> RecordingRetrieveResponse + +## Uploads + +Types: + +```python +from browserbase.types.sessions import UploadCreateResponse +``` + +Methods: + +- client.sessions.uploads.create(id, \*\*params) -> UploadCreateResponse diff --git a/bin/publish-pypi b/bin/publish-pypi new file mode 100644 index 00000000..05bfccbb --- /dev/null +++ b/bin/publish-pypi @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +set -eux +mkdir -p dist +rye build --clean +# Patching importlib-metadata version until upstream library version is updated +# https://github.com/pypa/twine/issues/977#issuecomment-2189800841 +"$HOME/.rye/self/bin/python3" -m pip install 'importlib-metadata==7.2.1' +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/mypy.ini b/mypy.ini new file mode 100644 index 00000000..94f60e5e --- /dev/null +++ b/mypy.ini @@ -0,0 +1,47 @@ +[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. +exclude = ^(src/browserbase/_files\.py|_dev/.*\.py)$ + +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 + +# https://github.com/python/mypy/issues/12162 +[mypy.overrides] +module = "black.files.*" +ignore_errors = true +ignore_missing_imports = true 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..26391407 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,208 @@ +[project] +name = "browserbase" +version = "0.0.1-alpha.0" +description = "The official Python library for the browserbase API" +dynamic = ["readme"] +license = "Apache-2.0" +authors = [ +{ name = "Browserbase", email = "dev-feedback@browserbase.com" }, +] +dependencies = [ + "httpx>=0.23.0, <1", + "pydantic>=1.9.0, <3", + "typing-extensions>=4.7, <5", + "anyio>=3.5.0, <5", + "distro>=1.7.0, <2", + "sniffio", + "cached-property; python_version < '3.8'", +] +requires-python = ">= 3.7" +classifiers = [ + "Typing :: Typed", + "Intended Audience :: Developers", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "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/browserbase-python" +Repository = "https://github.com/stainless-sdks/browserbase-python" + + + +[tool.rye] +managed = true +# version pins are in requirements-dev.lock +dev-dependencies = [ + "pyright>=1.1.359", + "mypy", + "respx", + "pytest", + "pytest-asyncio", + "ruff", + "time-machine", + "nox", + "dirty-equals>=0.6.0", + "importlib-metadata>=6.7.0", + "rich>=13.7.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 browserbase'" + +typecheck = { chain = [ + "typecheck:pyright", + "typecheck:mypy" +]} +"typecheck:pyright" = "pyright" +"typecheck:verify-types" = "pyright --verifytypes browserbase --ignoreexternal" +"typecheck:mypy" = "mypy ." + +[build-system] +requires = ["hatchling", "hatch-fancy-pypi-readme"] +build-backend = "hatchling.build" + +[tool.hatch.build] +include = [ + "src/*" +] + +[tool.hatch.build.targets.wheel] +packages = ["src/browserbase"] + +[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/browserbase-python/tree/main/\g<2>)' + +[tool.pytest.ini_options] +testpaths = ["tests"] +addopts = "--tb=short" +xfail_strict = true +asyncio_mode = "auto" +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.7" + +exclude = [ + "_dev", + ".venv", + ".nox", +] + +reportImplicitOverride = true + +reportImportCycles = false +reportPrivateUsage = false + + +[tool.ruff] +line-length = 120 +output-format = "grouped" +target-version = "py37" + +[tool.ruff.format] +docstring-code-format = true + +[tool.ruff.lint] +select = [ + # isort + "I", + # bugbear rules + "B", + # remove unused imports + "F401", + # bare except statements + "E722", + # unused arguments + "ARG", + # print statements + "T201", + "T203", + # misuse of typing.TYPE_CHECKING + "TCH004", + # import rules + "TID251", +] +ignore = [ + # mutable defaults + "B006", +] +unfixable = [ + # disable auto fix for print statements + "T201", + "T203", +] + +[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 = ["browserbase", "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..b6be61c0 --- /dev/null +++ b/requirements-dev.lock @@ -0,0 +1,105 @@ +# 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 + +-e file:. +annotated-types==0.6.0 + # via pydantic +anyio==4.4.0 + # via browserbase + # via httpx +argcomplete==3.1.2 + # via nox +attrs==23.1.0 + # via pytest +certifi==2023.7.22 + # via httpcore + # via httpx +colorlog==6.7.0 + # via nox +dirty-equals==0.6.0 +distlib==0.3.7 + # via virtualenv +distro==1.8.0 + # via browserbase +exceptiongroup==1.1.3 + # via anyio +filelock==3.12.4 + # via virtualenv +h11==0.14.0 + # via httpcore +httpcore==1.0.2 + # via httpx +httpx==0.25.2 + # via browserbase + # via respx +idna==3.4 + # via anyio + # via httpx +importlib-metadata==7.0.0 +iniconfig==2.0.0 + # via pytest +markdown-it-py==3.0.0 + # via rich +mdurl==0.1.2 + # via markdown-it-py +mypy==1.11.2 +mypy-extensions==1.0.0 + # via mypy +nodeenv==1.8.0 + # via pyright +nox==2023.4.22 +packaging==23.2 + # via nox + # via pytest +platformdirs==3.11.0 + # via virtualenv +pluggy==1.3.0 + # via pytest +py==1.11.0 + # via pytest +pydantic==2.7.1 + # via browserbase +pydantic-core==2.18.2 + # via pydantic +pygments==2.18.0 + # via rich +pyright==1.1.380 +pytest==7.1.1 + # via pytest-asyncio +pytest-asyncio==0.21.1 +python-dateutil==2.8.2 + # via time-machine +pytz==2023.3.post1 + # via dirty-equals +respx==0.20.2 +rich==13.7.1 +ruff==0.6.9 +setuptools==68.2.2 + # via nodeenv +six==1.16.0 + # via python-dateutil +sniffio==1.3.0 + # via anyio + # via browserbase + # via httpx +time-machine==2.9.0 +tomli==2.0.1 + # via mypy + # via pytest +typing-extensions==4.8.0 + # via anyio + # via browserbase + # via mypy + # via pydantic + # via pydantic-core +virtualenv==20.24.5 + # via nox +zipp==3.17.0 + # via importlib-metadata diff --git a/requirements.lock b/requirements.lock new file mode 100644 index 00000000..44b55d0d --- /dev/null +++ b/requirements.lock @@ -0,0 +1,45 @@ +# 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 + +-e file:. +annotated-types==0.6.0 + # via pydantic +anyio==4.4.0 + # via browserbase + # via httpx +certifi==2023.7.22 + # via httpcore + # via httpx +distro==1.8.0 + # via browserbase +exceptiongroup==1.1.3 + # via anyio +h11==0.14.0 + # via httpcore +httpcore==1.0.2 + # via httpx +httpx==0.25.2 + # via browserbase +idna==3.4 + # via anyio + # via httpx +pydantic==2.7.1 + # via browserbase +pydantic-core==2.18.2 + # via pydantic +sniffio==1.3.0 + # via anyio + # via browserbase + # via httpx +typing-extensions==4.8.0 + # via anyio + # via browserbase + # via pydantic + # via pydantic-core diff --git a/scripts/bootstrap b/scripts/bootstrap new file mode 100755 index 00000000..8c5c60eb --- /dev/null +++ b/scripts/bootstrap @@ -0,0 +1,19 @@ +#!/usr/bin/env bash + +set -e + +cd "$(dirname "$0")/.." + +if [ -f "Brewfile" ] && [ "$(uname -s)" = "Darwin" ]; then + brew bundle check >/dev/null 2>&1 || { + echo "==> Installing Homebrew dependencies…" + brew bundle + } +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..a74a1988 --- /dev/null +++ b/scripts/lint @@ -0,0 +1,12 @@ +#!/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 browserbase' + diff --git a/scripts/mock b/scripts/mock new file mode 100755 index 00000000..d2814ae6 --- /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.8.5 -- 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.8.5 -- prism mock "$URL" +fi diff --git a/scripts/test b/scripts/test new file mode 100755 index 00000000..4fa5698b --- /dev/null +++ b/scripts/test @@ -0,0 +1,59 @@ +#!/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=@stoplight/prism-cli@~5.3.2 -- 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 + +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..37b3d94f --- /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/src/browserbase/__init__.py b/src/browserbase/__init__.py new file mode 100644 index 00000000..4b1d2804 --- /dev/null +++ b/src/browserbase/__init__.py @@ -0,0 +1,93 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from . import types +from ._types import NOT_GIVEN, NoneType, NotGiven, Transport, ProxiesTypes +from ._utils import file_from_path +from ._client import ( + Client, + Stream, + Timeout, + Transport, + AsyncClient, + AsyncStream, + Browserbase, + RequestOptions, + AsyncBrowserbase, +) +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, + APITimeoutError, + BadRequestError, + BrowserbaseError, + APIConnectionError, + AuthenticationError, + InternalServerError, + PermissionDeniedError, + UnprocessableEntityError, + APIResponseValidationError, +) +from ._base_client import DefaultHttpxClient, DefaultAsyncHttpxClient +from ._utils._logs import setup_logging as _setup_logging + +__all__ = [ + "types", + "__version__", + "__title__", + "NoneType", + "Transport", + "ProxiesTypes", + "NotGiven", + "NOT_GIVEN", + "BrowserbaseError", + "APIError", + "APIStatusError", + "APITimeoutError", + "APIConnectionError", + "APIResponseValidationError", + "BadRequestError", + "AuthenticationError", + "PermissionDeniedError", + "NotFoundError", + "ConflictError", + "UnprocessableEntityError", + "RateLimitError", + "InternalServerError", + "Timeout", + "RequestOptions", + "Client", + "AsyncClient", + "Stream", + "AsyncStream", + "Browserbase", + "AsyncBrowserbase", + "file_from_path", + "BaseModel", + "DEFAULT_TIMEOUT", + "DEFAULT_MAX_RETRIES", + "DEFAULT_CONNECTION_LIMITS", + "DefaultHttpxClient", + "DefaultAsyncHttpxClient", +] + +_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. +# browserbase._exceptions.NotFoundError -> browserbase.NotFoundError +__locals = locals() +for __name in __all__: + if not __name.startswith("__"): + try: + __locals[__name].__module__ = "browserbase" + except (TypeError, AttributeError): + # Some of our exported symbols are builtins which we can't set attributes for. + pass diff --git a/src/browserbase/_base_client.py b/src/browserbase/_base_client.py new file mode 100644 index 00000000..f17e8d2b --- /dev/null +++ b/src/browserbase/_base_client.py @@ -0,0 +1,2041 @@ +from __future__ import annotations + +import sys +import json +import time +import uuid +import email +import asyncio +import inspect +import logging +import platform +import warnings +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, Limits +from pydantic import PrivateAttr + +from . import _exceptions +from ._qs import Querystring +from ._files import to_httpx_files, async_to_httpx_files +from ._types import ( + NOT_GIVEN, + Body, + Omit, + Query, + Headers, + Timeout, + NotGiven, + ResponseT, + Transport, + AnyMapping, + PostParser, + ProxiesTypes, + RequestFiles, + HttpxSendArgs, + AsyncTransport, + RequestOptions, + HttpxRequestFiles, + ModelBuilderProtocol, +) +from ._utils import is_dict, is_list, asyncify, is_given, lru_cache, is_mapping +from ._compat import 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 as HTTPX_DEFAULT_TIMEOUT +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 + + @overload + def __init__( + self, + *, + url: URL, + ) -> None: ... + + @overload + def __init__( + self, + *, + params: Query, + ) -> None: ... + + def __init__( + self, + *, + url: URL | NotGiven = NOT_GIVEN, + params: Query | NotGiven = NOT_GIVEN, + ) -> None: + self.url = url + self.params = params + + @override + def __repr__(self) -> str: + if self.url: + return f"{self.__class__.__name__}(url={self.url})" + 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 + + 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: + 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: + 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] + _limits: httpx.Limits + _proxies: ProxiesTypes | None + _transport: Transport | AsyncTransport | 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, + limits: httpx.Limits, + transport: Transport | AsyncTransport | None, + proxies: ProxiesTypes | None, + 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._limits = limits + self._proxies = proxies + self._transport = transport + 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 `browserbase.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.method.lower() != "get" and idempotency_header not in headers: + headers[idempotency_header] = options.idempotency_key or self._idempotency_key() + + # Don't set the retry count header if it was already set or removed by the caller. We check + # `custom_headers`, which can contain `Omit()`, instead of `headers` to account for the removal case. + if "x-stainless-retry-count" not in (header.lower() for header in custom_headers): + headers["x-stainless-retry-count"] = str(retries_taken) + + 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("_", "-")} + + # 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, + json=json_data, + files=files, + **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: + 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, + transport: Transport | None = None, + proxies: ProxiesTypes | None = None, + limits: Limits | None = None, + 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 limits is not None: + warnings.warn( + "The `connection_pool_limits` argument is deprecated. The `http_client` argument should be passed instead", + category=DeprecationWarning, + stacklevel=3, + ) + if http_client is not None: + raise ValueError("The `http_client` argument is mutually exclusive with `connection_pool_limits`") + else: + limits = DEFAULT_CONNECTION_LIMITS + + if transport is not None: + warnings.warn( + "The `transport` argument is deprecated. The `http_client` argument should be passed instead", + category=DeprecationWarning, + stacklevel=3, + ) + if http_client is not None: + raise ValueError("The `http_client` argument is mutually exclusive with `transport`") + + if proxies is not None: + warnings.warn( + "The `proxies` argument is deprecated. The `http_client` argument should be passed instead", + category=DeprecationWarning, + stacklevel=3, + ) + if http_client is not None: + raise ValueError("The `http_client` argument is mutually exclusive with `proxies`") + + 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, + limits=limits, + # cast to a valid type because mypy doesn't understand our type narrowing + timeout=cast(Timeout, timeout), + proxies=proxies, + base_url=base_url, + transport=transport, + 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), + proxies=proxies, + transport=transport, + limits=limits, + follow_redirects=True, + ) + + 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, + remaining_retries: Optional[int] = None, + *, + stream: Literal[True], + stream_cls: Type[_StreamT], + ) -> _StreamT: ... + + @overload + def request( + self, + cast_to: Type[ResponseT], + options: FinalRequestOptions, + remaining_retries: Optional[int] = None, + *, + stream: Literal[False] = False, + ) -> ResponseT: ... + + @overload + def request( + self, + cast_to: Type[ResponseT], + options: FinalRequestOptions, + remaining_retries: Optional[int] = None, + *, + stream: bool = False, + stream_cls: Type[_StreamT] | None = None, + ) -> ResponseT | _StreamT: ... + + def request( + self, + cast_to: Type[ResponseT], + options: FinalRequestOptions, + remaining_retries: Optional[int] = None, + *, + stream: bool = False, + stream_cls: type[_StreamT] | None = None, + ) -> ResponseT | _StreamT: + if remaining_retries is not None: + retries_taken = options.get_max_retries(self.max_retries) - remaining_retries + else: + retries_taken = 0 + + return self._request( + cast_to=cast_to, + options=options, + stream=stream, + stream_cls=stream_cls, + retries_taken=retries_taken, + ) + + def _request( + self, + *, + cast_to: Type[ResponseT], + options: FinalRequestOptions, + retries_taken: int, + stream: bool, + stream_cls: type[_StreamT] | None, + ) -> ResponseT | _StreamT: + # 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) + + cast_to = self._maybe_override_cast_to(cast_to, options) + options = self._prepare_options(options) + + remaining_retries = options.get_max_retries(self.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 + + log.debug("Sending HTTP Request: %s %s", request.method, request.url) + + 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: + return self._retry_request( + input_options, + cast_to, + retries_taken=retries_taken, + stream=stream, + stream_cls=stream_cls, + response_headers=None, + ) + + 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: + return self._retry_request( + input_options, + cast_to, + retries_taken=retries_taken, + stream=stream, + stream_cls=stream_cls, + response_headers=None, + ) + + 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() + return self._retry_request( + input_options, + cast_to, + retries_taken=retries_taken, + response_headers=err.response.headers, + stream=stream, + stream_cls=stream_cls, + ) + + # 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 + + return self._process_response( + cast_to=cast_to, + options=options, + response=response, + stream=stream, + stream_cls=stream_cls, + retries_taken=retries_taken, + ) + + def _retry_request( + self, + options: FinalRequestOptions, + cast_to: Type[ResponseT], + *, + retries_taken: int, + response_headers: httpx.Headers | None, + stream: bool, + stream_cls: type[_StreamT] | None, + ) -> ResponseT | _StreamT: + remaining_retries = options.get_max_retries(self.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) + log.info("Retrying request to %s in %f seconds", options.url, timeout) + + # In a synchronous context we are blocking the entire thread. Up to the library user to run the client in a + # different thread if necessary. + time.sleep(timeout) + + return self._request( + options=options, + cast_to=cast_to, + retries_taken=retries_taken + 1, + stream=stream, + stream_cls=stream_cls, + ) + + 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): + 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) + + +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. + """ +else: + DefaultAsyncHttpxClient = _DefaultAsyncHttpxClient + + +class AsyncHttpxClientWrapper(DefaultAsyncHttpxClient): + def __del__(self) -> None: + 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, + transport: AsyncTransport | None = None, + proxies: ProxiesTypes | None = None, + limits: Limits | None = None, + http_client: httpx.AsyncClient | None = None, + custom_headers: Mapping[str, str] | None = None, + custom_query: Mapping[str, object] | None = None, + ) -> None: + if limits is not None: + warnings.warn( + "The `connection_pool_limits` argument is deprecated. The `http_client` argument should be passed instead", + category=DeprecationWarning, + stacklevel=3, + ) + if http_client is not None: + raise ValueError("The `http_client` argument is mutually exclusive with `connection_pool_limits`") + else: + limits = DEFAULT_CONNECTION_LIMITS + + if transport is not None: + warnings.warn( + "The `transport` argument is deprecated. The `http_client` argument should be passed instead", + category=DeprecationWarning, + stacklevel=3, + ) + if http_client is not None: + raise ValueError("The `http_client` argument is mutually exclusive with `transport`") + + if proxies is not None: + warnings.warn( + "The `proxies` argument is deprecated. The `http_client` argument should be passed instead", + category=DeprecationWarning, + stacklevel=3, + ) + if http_client is not None: + raise ValueError("The `http_client` argument is mutually exclusive with `proxies`") + + 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, + limits=limits, + # cast to a valid type because mypy doesn't understand our type narrowing + timeout=cast(Timeout, timeout), + proxies=proxies, + transport=transport, + 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), + proxies=proxies, + transport=transport, + limits=limits, + follow_redirects=True, + ) + + 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, + remaining_retries: Optional[int] = None, + ) -> ResponseT: ... + + @overload + async def request( + self, + cast_to: Type[ResponseT], + options: FinalRequestOptions, + *, + stream: Literal[True], + stream_cls: type[_AsyncStreamT], + remaining_retries: Optional[int] = None, + ) -> _AsyncStreamT: ... + + @overload + async def request( + self, + cast_to: Type[ResponseT], + options: FinalRequestOptions, + *, + stream: bool, + stream_cls: type[_AsyncStreamT] | None = None, + remaining_retries: Optional[int] = None, + ) -> ResponseT | _AsyncStreamT: ... + + async def request( + self, + cast_to: Type[ResponseT], + options: FinalRequestOptions, + *, + stream: bool = False, + stream_cls: type[_AsyncStreamT] | None = None, + remaining_retries: Optional[int] = None, + ) -> ResponseT | _AsyncStreamT: + if remaining_retries is not None: + retries_taken = options.get_max_retries(self.max_retries) - remaining_retries + else: + retries_taken = 0 + + return await self._request( + cast_to=cast_to, + options=options, + stream=stream, + stream_cls=stream_cls, + retries_taken=retries_taken, + ) + + async def _request( + self, + cast_to: Type[ResponseT], + options: FinalRequestOptions, + *, + stream: bool, + stream_cls: type[_AsyncStreamT] | None, + retries_taken: int, + ) -> 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)() + + # 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) + + cast_to = self._maybe_override_cast_to(cast_to, options) + options = await self._prepare_options(options) + + remaining_retries = options.get_max_retries(self.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 + + 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: + return await self._retry_request( + input_options, + cast_to, + retries_taken=retries_taken, + stream=stream, + stream_cls=stream_cls, + response_headers=None, + ) + + 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: + return await self._retry_request( + input_options, + cast_to, + retries_taken=retries_taken, + stream=stream, + stream_cls=stream_cls, + response_headers=None, + ) + + log.debug("Raising connection error") + raise APIConnectionError(request=request) from err + + log.debug( + 'HTTP Request: %s %s "%i %s"', request.method, request.url, response.status_code, response.reason_phrase + ) + + 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() + return await self._retry_request( + input_options, + cast_to, + retries_taken=retries_taken, + response_headers=err.response.headers, + stream=stream, + stream_cls=stream_cls, + ) + + # 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 + + 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 _retry_request( + self, + options: FinalRequestOptions, + cast_to: Type[ResponseT], + *, + retries_taken: int, + response_headers: httpx.Headers | None, + stream: bool, + stream_cls: type[_AsyncStreamT] | None, + ) -> ResponseT | _AsyncStreamT: + remaining_retries = options.get_max_retries(self.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) + log.info("Retrying request to %s in %f seconds", options.url, timeout) + + await anyio.sleep(timeout) + + return await self._request( + options=options, + cast_to=cast_to, + retries_taken=retries_taken + 1, + stream=stream, + stream_cls=stream_cls, + ) + + 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): + 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/browserbase/_client.py b/src/browserbase/_client.py new file mode 100644 index 00000000..b0d011bb --- /dev/null +++ b/src/browserbase/_client.py @@ -0,0 +1,430 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import os +from typing import Any, Union, Mapping +from typing_extensions import Self, override + +import httpx + +from . import resources, _exceptions +from ._qs import Querystring +from ._types import ( + NOT_GIVEN, + Omit, + Timeout, + NotGiven, + Transport, + ProxiesTypes, + RequestOptions, +) +from ._utils import ( + is_given, + get_async_library, +) +from ._version import __version__ +from ._streaming import Stream as Stream, AsyncStream as AsyncStream +from ._exceptions import APIStatusError, BrowserbaseError +from ._base_client import ( + DEFAULT_MAX_RETRIES, + SyncAPIClient, + AsyncAPIClient, +) + +__all__ = [ + "Timeout", + "Transport", + "ProxiesTypes", + "RequestOptions", + "resources", + "Browserbase", + "AsyncBrowserbase", + "Client", + "AsyncClient", +] + + +class Browserbase(SyncAPIClient): + contexts: resources.ContextsResource + extensions: resources.ExtensionsResource + projects: resources.ProjectsResource + sessions: resources.SessionsResource + with_raw_response: BrowserbaseWithRawResponse + with_streaming_response: BrowserbaseWithStreamedResponse + + # client options + api_key: str + + def __init__( + self, + *, + api_key: str | None = None, + base_url: str | httpx.URL | None = None, + timeout: Union[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 browserbase client instance. + + This automatically infers the `api_key` argument from the `BROWSERBASE_API_KEY` environment variable if it is not provided. + """ + if api_key is None: + api_key = os.environ.get("BROWSERBASE_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 BROWSERBASE_API_KEY environment variable" + ) + self.api_key = api_key + + if base_url is None: + base_url = os.environ.get("BROWSERBASE_BASE_URL") + if base_url is None: + base_url = f"https://www.browserbase.com" + + 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.contexts = resources.ContextsResource(self) + self.extensions = resources.ExtensionsResource(self) + self.projects = resources.ProjectsResource(self) + self.sessions = resources.SessionsResource(self) + self.with_raw_response = BrowserbaseWithRawResponse(self) + self.with_streaming_response = BrowserbaseWithStreamedResponse(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 + return {"X-BB-API-Key": api_key} + + @property + @override + def default_headers(self) -> dict[str, str | Omit]: + return { + **super().default_headers, + "X-Stainless-Async": "false", + **self._custom_headers, + } + + def copy( + self, + *, + 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, + 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, + 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 AsyncBrowserbase(AsyncAPIClient): + contexts: resources.AsyncContextsResource + extensions: resources.AsyncExtensionsResource + projects: resources.AsyncProjectsResource + sessions: resources.AsyncSessionsResource + with_raw_response: AsyncBrowserbaseWithRawResponse + with_streaming_response: AsyncBrowserbaseWithStreamedResponse + + # client options + api_key: str + + def __init__( + self, + *, + api_key: str | None = None, + base_url: str | httpx.URL | None = None, + timeout: Union[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 browserbase client instance. + + This automatically infers the `api_key` argument from the `BROWSERBASE_API_KEY` environment variable if it is not provided. + """ + if api_key is None: + api_key = os.environ.get("BROWSERBASE_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 BROWSERBASE_API_KEY environment variable" + ) + self.api_key = api_key + + if base_url is None: + base_url = os.environ.get("BROWSERBASE_BASE_URL") + if base_url is None: + base_url = f"https://www.browserbase.com" + + 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.contexts = resources.AsyncContextsResource(self) + self.extensions = resources.AsyncExtensionsResource(self) + self.projects = resources.AsyncProjectsResource(self) + self.sessions = resources.AsyncSessionsResource(self) + self.with_raw_response = AsyncBrowserbaseWithRawResponse(self) + self.with_streaming_response = AsyncBrowserbaseWithStreamedResponse(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 + return {"X-BB-API-Key": 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, + } + + def copy( + self, + *, + 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, + 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, + 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 BrowserbaseWithRawResponse: + def __init__(self, client: Browserbase) -> None: + self.contexts = resources.ContextsResourceWithRawResponse(client.contexts) + self.extensions = resources.ExtensionsResourceWithRawResponse(client.extensions) + self.projects = resources.ProjectsResourceWithRawResponse(client.projects) + self.sessions = resources.SessionsResourceWithRawResponse(client.sessions) + + +class AsyncBrowserbaseWithRawResponse: + def __init__(self, client: AsyncBrowserbase) -> None: + self.contexts = resources.AsyncContextsResourceWithRawResponse(client.contexts) + self.extensions = resources.AsyncExtensionsResourceWithRawResponse(client.extensions) + self.projects = resources.AsyncProjectsResourceWithRawResponse(client.projects) + self.sessions = resources.AsyncSessionsResourceWithRawResponse(client.sessions) + + +class BrowserbaseWithStreamedResponse: + def __init__(self, client: Browserbase) -> None: + self.contexts = resources.ContextsResourceWithStreamingResponse(client.contexts) + self.extensions = resources.ExtensionsResourceWithStreamingResponse(client.extensions) + self.projects = resources.ProjectsResourceWithStreamingResponse(client.projects) + self.sessions = resources.SessionsResourceWithStreamingResponse(client.sessions) + + +class AsyncBrowserbaseWithStreamedResponse: + def __init__(self, client: AsyncBrowserbase) -> None: + self.contexts = resources.AsyncContextsResourceWithStreamingResponse(client.contexts) + self.extensions = resources.AsyncExtensionsResourceWithStreamingResponse(client.extensions) + self.projects = resources.AsyncProjectsResourceWithStreamingResponse(client.projects) + self.sessions = resources.AsyncSessionsResourceWithStreamingResponse(client.sessions) + + +Client = Browserbase + +AsyncClient = AsyncBrowserbase diff --git a/src/browserbase/_compat.py b/src/browserbase/_compat.py new file mode 100644 index 00000000..162a6fbe --- /dev/null +++ b/src/browserbase/_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 + +import pydantic +from pydantic.fields import FieldInfo + +from ._types import IncEx, StrBytesIntFloat + +_T = TypeVar("_T") +_ModelT = TypeVar("_ModelT", bound=pydantic.BaseModel) + +# --------------- Pydantic v2 compatibility --------------- + +# Pyright incorrectly reports some of our functions as overriding a method when they don't +# pyright: reportIncompatibleMethodOverride=false + +PYDANTIC_V2 = pydantic.VERSION.startswith("2.") + +# v1 re-exports +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: + if PYDANTIC_V2: + from pydantic.v1.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.v1.datetime_parse import parse_date as parse_date, parse_datetime as parse_datetime + else: + 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 + + +# refactored config +if TYPE_CHECKING: + from pydantic import ConfigDict as ConfigDict +else: + if PYDANTIC_V2: + from pydantic import ConfigDict + else: + # TODO: provide an error message here? + ConfigDict = None + + +# renamed methods / properties +def parse_obj(model: type[_ModelT], value: object) -> _ModelT: + if PYDANTIC_V2: + return model.model_validate(value) + else: + return cast(_ModelT, model.parse_obj(value)) # pyright: ignore[reportDeprecated, reportUnnecessaryCast] + + +def field_is_required(field: FieldInfo) -> bool: + if PYDANTIC_V2: + return field.is_required() + return field.required # type: ignore + + +def field_get_default(field: FieldInfo) -> Any: + value = field.get_default() + if PYDANTIC_V2: + from pydantic_core import PydanticUndefined + + if value == PydanticUndefined: + return None + return value + return value + + +def field_outer_type(field: FieldInfo) -> Any: + if PYDANTIC_V2: + return field.annotation + return field.outer_type_ # type: ignore + + +def get_model_config(model: type[pydantic.BaseModel]) -> Any: + if PYDANTIC_V2: + return model.model_config + return model.__config__ # type: ignore + + +def get_model_fields(model: type[pydantic.BaseModel]) -> dict[str, FieldInfo]: + if PYDANTIC_V2: + return model.model_fields + return model.__fields__ # type: ignore + + +def model_copy(model: _ModelT, *, deep: bool = False) -> _ModelT: + if PYDANTIC_V2: + return model.model_copy(deep=deep) + return model.copy(deep=deep) # type: ignore + + +def model_json(model: pydantic.BaseModel, *, indent: int | None = None) -> str: + if PYDANTIC_V2: + return model.model_dump_json(indent=indent) + return model.json(indent=indent) # type: ignore + + +def model_dump( + model: pydantic.BaseModel, + *, + exclude: IncEx = None, + exclude_unset: bool = False, + exclude_defaults: bool = False, + warnings: bool = True, +) -> dict[str, Any]: + if PYDANTIC_V2: + return model.model_dump( + exclude=exclude, + exclude_unset=exclude_unset, + exclude_defaults=exclude_defaults, + warnings=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_V2: + return model.model_validate(data) + return model.parse_obj(data) # pyright: ignore[reportDeprecated] + + +# generic models +if TYPE_CHECKING: + + class GenericModel(pydantic.BaseModel): ... + +else: + if PYDANTIC_V2: + # 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): ... + + else: + import pydantic.generics + + class GenericModel(pydantic.generics.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: + try: + from functools import cached_property as cached_property + except ImportError: + from cached_property import cached_property as cached_property + + typed_cached_property = cached_property diff --git a/src/browserbase/_constants.py b/src/browserbase/_constants.py new file mode 100644 index 00000000..a2ac3b6f --- /dev/null +++ b/src/browserbase/_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.0, 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/browserbase/_exceptions.py b/src/browserbase/_exceptions.py new file mode 100644 index 00000000..79b18ef7 --- /dev/null +++ b/src/browserbase/_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 BrowserbaseError(Exception): + pass + + +class APIError(BrowserbaseError): + 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/browserbase/_files.py b/src/browserbase/_files.py new file mode 100644 index 00000000..715cc207 --- /dev/null +++ b/src/browserbase/_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/browserbase/_models.py b/src/browserbase/_models.py new file mode 100644 index 00000000..d386eaa3 --- /dev/null +++ b/src/browserbase/_models.py @@ -0,0 +1,785 @@ +from __future__ import annotations + +import os +import inspect +from typing import TYPE_CHECKING, Any, Type, Union, Generic, TypeVar, Callable, cast +from datetime import date, datetime +from typing_extensions import ( + Unpack, + Literal, + ClassVar, + Protocol, + Required, + ParamSpec, + TypedDict, + TypeGuard, + final, + override, + runtime_checkable, +) + +import pydantic +import pydantic.generics +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, + lru_cache, + is_mapping, + parse_date, + coerce_boolean, + parse_datetime, + strip_not_given, + extract_type_arg, + is_annotated_type, + strip_annotated_type, +) +from ._compat import ( + PYDANTIC_V2, + 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, 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_V2: + model_config: ClassVar[ConfigDict] = ConfigDict( + extra="allow", defer_build=coerce_boolean(os.environ.get("DEFER_PYDANTIC_BUILD", "true")) + ) + else: + + @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 + + 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( + 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 = {} + for key, value in values.items(): + if key not in model_fields: + if PYDANTIC_V2: + _extra[key] = value + else: + _fields_set.add(key) + fields_values[key] = value + + object.__setattr__(m, "__dict__", fields_values) + + if PYDANTIC_V2: + # 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) + else: + # 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) + + 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 not PYDANTIC_V2: + # 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, + exclude: IncEx = None, + by_alias: bool = False, + exclude_unset: bool = False, + exclude_defaults: bool = False, + exclude_none: bool = False, + round_trip: bool = False, + warnings: bool | Literal["none", "warn", "error"] = True, + context: dict[str, 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 dictionary will only contain JSON serializable types. + If mode is 'python', the dictionary may contain any Python objects. + include: A list of fields to include in the output. + exclude: A list of fields to exclude from the output. + by_alias: Whether to use the field's alias in the dictionary key if defined. + exclude_unset: Whether to exclude fields that are unset or None from the output. + 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. + round_trip: Whether to enable serialization and deserialization round-trip support. + warnings: Whether to log warnings when invalid fields are encountered. + + Returns: + A dictionary representation of the model. + """ + if mode != "python": + raise ValueError("mode is only supported in Pydantic v2") + 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") + return super().dict( # pyright: ignore[reportDeprecated] + include=include, + exclude=exclude, + by_alias=by_alias, + exclude_unset=exclude_unset, + exclude_defaults=exclude_defaults, + exclude_none=exclude_none, + ) + + @override + def model_dump_json( + self, + *, + indent: int | None = None, + include: IncEx = None, + exclude: IncEx = None, + by_alias: bool = False, + exclude_unset: bool = False, + exclude_defaults: bool = False, + exclude_none: bool = False, + round_trip: bool = False, + warnings: bool | Literal["none", "warn", "error"] = True, + context: dict[str, 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") + return super().json( # type: ignore[reportDeprecated] + indent=indent, + include=include, + exclude=exclude, + by_alias=by_alias, + 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_V2: + type_ = field.annotation + else: + type_ = cast(type, field.outer_type_) # type: ignore + + if type_ is None: + raise RuntimeError(f"Unexpected field type is None for {key}") + + return construct_type(value=value, type_=type_) + + +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) -> 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. + """ + # 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_) + + # unwrap `Annotated[T, ...]` -> `T` + if is_annotated_type(type_): + meta: tuple[Any, ...] = 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]", 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 (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 + + +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: + if isinstance(union, CachedDiscriminatorType): + return union.__discriminator__ + + 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_V2: + 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 + else: + 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 field_info.annotation and is_literal_type(field_info.annotation): + for entry in get_args(field_info.annotation): + 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, + ) + cast(CachedDiscriminatorType, union).__discriminator__ = details + return details + + +def _extract_field_schema_pv2(model: type[BaseModel], field_name: str) -> ModelField | None: + schema = model.__pydantic_core_schema__ + if schema["type"] != "model": + return None + + 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 subclasssing 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 PYDANTIC_V2: + 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 + + +@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() + + # 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_V2: + model_config: ClassVar[ConfigDict] = ConfigDict(arbitrary_types_allowed=True) + else: + + class Config(pydantic.BaseConfig): # pyright: ignore[reportDeprecated] + arbitrary_types_allowed: bool = 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_V2: + return super().model_construct(_fields_set, **kwargs) + return cast(FinalRequestOptions, super().construct(_fields_set, **kwargs)) # pyright: ignore[reportDeprecated] + + if not TYPE_CHECKING: + # type checkers incorrectly complain about this assignment + model_construct = construct diff --git a/src/browserbase/_qs.py b/src/browserbase/_qs.py new file mode 100644 index 00000000..274320ca --- /dev/null +++ b/src/browserbase/_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 NOT_GIVEN, NotGiven, NotGivenOr +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: NotGivenOr[ArrayFormat] = NOT_GIVEN, + nested_format: NotGivenOr[NestedFormat] = 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: NotGivenOr[ArrayFormat] = NOT_GIVEN, + nested_format: NotGivenOr[NestedFormat] = 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: NotGivenOr[ArrayFormat] = NOT_GIVEN, + nested_format: NotGivenOr[NestedFormat] = 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/browserbase/_resource.py b/src/browserbase/_resource.py new file mode 100644 index 00000000..fc2d2a11 --- /dev/null +++ b/src/browserbase/_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 Browserbase, AsyncBrowserbase + + +class SyncAPIResource: + _client: Browserbase + + def __init__(self, client: Browserbase) -> 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: AsyncBrowserbase + + def __init__(self, client: AsyncBrowserbase) -> 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/browserbase/_response.py b/src/browserbase/_response.py new file mode 100644 index 00000000..81ae0828 --- /dev/null +++ b/src/browserbase/_response.py @@ -0,0 +1,826 @@ +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, 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 BrowserbaseError, 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: + # unwrap `Annotated[T, ...]` -> `T` + if to and is_annotated_type(to): + to = extract_type_arg(to, 0) + + 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=self._cast_to, + response=self.http_response, + client=cast(Any, self._client), + ), + ) + + cast_to = to if to is not None else self._cast_to + + # unwrap `Annotated[T, ...]` -> `T` + if is_annotated_type(cast_to): + cast_to = extract_type_arg(cast_to, 0) + + 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") + + origin = get_origin(cast_to) or cast_to + + 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) and not issubclass(origin, BaseModel) and issubclass(origin, pydantic.BaseModel): + raise TypeError( + "Pydantic models must subclass our base model type, e.g. `from browserbase 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 content_type != "application/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 browserbase 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 browserbase 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 `browserbase._streaming` for reference", + ) + + +class StreamAlreadyConsumed(BrowserbaseError): + """ + 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/browserbase/_streaming.py b/src/browserbase/_streaming.py new file mode 100644 index 00000000..c04b2332 --- /dev/null +++ b/src/browserbase/_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 Browserbase, AsyncBrowserbase + + +_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: Browserbase, + ) -> 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() + + for sse in iterator: + yield process_data(data=sse.json(), cast_to=cast_to, response=response) + + # Ensure the entire stream is consumed + for _sse in iterator: + ... + + 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: AsyncBrowserbase, + ) -> 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() + + async for sse in iterator: + yield process_data(data=sse.json(), cast_to=cast_to, response=response) + + # Ensure the entire stream is consumed + async for _sse in iterator: + ... + + 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/browserbase/_types.py b/src/browserbase/_types.py new file mode 100644 index 00000000..5ab69853 --- /dev/null +++ b/src/browserbase/_types.py @@ -0,0 +1,217 @@ +from __future__ import annotations + +from os import PathLike +from typing import ( + IO, + TYPE_CHECKING, + Any, + Dict, + List, + Type, + Tuple, + Union, + Mapping, + TypeVar, + Callable, + Optional, + Sequence, +) +from typing_extensions import Literal, Protocol, TypeAlias, TypedDict, 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 browserbase 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 + + +# Sentinel class used until PEP 0661 is accepted +class NotGiven: + """ + A sentinel singleton class used to distinguish omitted keyword arguments + from those passed in with the value None (which may have different behavior). + + For example: + + ```py + def get(timeout: Union[int, NotGiven, None] = NotGiven()) -> Response: ... + + + get(timeout=1) # 1s timeout + get(timeout=None) # No timeout + get() # Default timeout behavior, which may not be statically known at the method definition. + ``` + """ + + def __bool__(self) -> Literal[False]: + return False + + @override + def __repr__(self) -> str: + return "NOT_GIVEN" + + +NotGivenOr = Union[_T, NotGiven] +NOT_GIVEN = NotGiven() + + +class Omit: + """In certain situations you need to be able to represent a case where a default value has + to be explicitly removed and `None` is not an appropriate substitute, for example: + + ```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 + + +@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/32ea570bf96e84234d2992e1ddf40ab8a565925a/pydantic/main.py#L49 +IncEx: TypeAlias = "set[int] | set[str] | dict[int, Any] | dict[str, Any] | None" + +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 diff --git a/src/browserbase/_utils/__init__.py b/src/browserbase/_utils/__init__.py new file mode 100644 index 00000000..3efe66c8 --- /dev/null +++ b/src/browserbase/_utils/__init__.py @@ -0,0 +1,55 @@ +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, + lru_cache as lru_cache, + is_mapping as is_mapping, + is_tuple_t as is_tuple_t, + parse_date as parse_date, + 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, + parse_datetime as parse_datetime, + 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 ._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_annotated_type as is_annotated_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, +) diff --git a/src/browserbase/_utils/_logs.py b/src/browserbase/_utils/_logs.py new file mode 100644 index 00000000..b527ee47 --- /dev/null +++ b/src/browserbase/_utils/_logs.py @@ -0,0 +1,25 @@ +import os +import logging + +logger: logging.Logger = logging.getLogger("browserbase") +httpx_logger: logging.Logger = logging.getLogger("httpx") + + +def _basic_config() -> None: + # e.g. [2023-10-05 14:12:26 - browserbase._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("BROWSERBASE_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/browserbase/_utils/_proxy.py b/src/browserbase/_utils/_proxy.py new file mode 100644 index 00000000..ffd883e9 --- /dev/null +++ b/src/browserbase/_utils/_proxy.py @@ -0,0 +1,62 @@ +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 + proxied = self.__get_proxied__() + 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/browserbase/_utils/_reflection.py b/src/browserbase/_utils/_reflection.py new file mode 100644 index 00000000..89aa712a --- /dev/null +++ b/src/browserbase/_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/browserbase/_utils/_streams.py b/src/browserbase/_utils/_streams.py new file mode 100644 index 00000000..f4a0208f --- /dev/null +++ b/src/browserbase/_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/browserbase/_utils/_sync.py b/src/browserbase/_utils/_sync.py new file mode 100644 index 00000000..d0d81033 --- /dev/null +++ b/src/browserbase/_utils/_sync.py @@ -0,0 +1,81 @@ +from __future__ import annotations + +import functools +from typing import TypeVar, Callable, Awaitable +from typing_extensions import ParamSpec + +import anyio +import anyio.to_thread + +from ._reflection import function_has_argument + +T_Retval = TypeVar("T_Retval") +T_ParamSpec = ParamSpec("T_ParamSpec") + + +# copied from `asyncer`, https://github.com/tiangolo/asyncer +def asyncify( + function: Callable[T_ParamSpec, T_Retval], + *, + cancellable: bool = False, + limiter: anyio.CapacityLimiter | None = None, +) -> Callable[T_ParamSpec, Awaitable[T_Retval]]: + """ + Take a blocking function and create an async one that receives the same + positional and keyword arguments, and that when called, calls the original function + in a worker thread using `anyio.to_thread.run_sync()`. Internally, + `asyncer.asyncify()` uses the same `anyio.to_thread.run_sync()`, but it supports + keyword arguments additional to positional arguments and it adds better support for + autocompletion and inline errors for the arguments of the function called and the + return value. + + If the `cancellable` option is enabled and the task waiting for its completion is + cancelled, the thread will still run its course but its return value (or any raised + exception) will be ignored. + + Use it like this: + + ```Python + def do_work(arg1, arg2, kwarg1="", kwarg2="") -> str: + # Do work + return "Some result" + + + result = await to_thread.asyncify(do_work)("spam", "ham", kwarg1="a", kwarg2="b") + print(result) + ``` + + ## Arguments + + `function`: a blocking regular callable (e.g. a function) + `cancellable`: `True` to allow cancellation of the operation + `limiter`: capacity limiter to use to limit the total amount of threads running + (if omitted, the default limiter is used) + + ## 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: + partial_f = functools.partial(function, *args, **kwargs) + + # In `v4.1.0` anyio added the `abandon_on_cancel` argument and deprecated the old + # `cancellable` argument, so we need to use the new `abandon_on_cancel` to avoid + # surfacing deprecation warnings. + if function_has_argument(anyio.to_thread.run_sync, "abandon_on_cancel"): + return await anyio.to_thread.run_sync( + partial_f, + abandon_on_cancel=cancellable, + limiter=limiter, + ) + + return await anyio.to_thread.run_sync( + partial_f, + cancellable=cancellable, + limiter=limiter, + ) + + return wrapper diff --git a/src/browserbase/_utils/_transform.py b/src/browserbase/_utils/_transform.py new file mode 100644 index 00000000..47e262a5 --- /dev/null +++ b/src/browserbase/_utils/_transform.py @@ -0,0 +1,382 @@ +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 + +import anyio +import pydantic + +from ._utils import ( + is_list, + is_mapping, + is_iterable, +) +from .._files import is_base64_file_input +from ._typing import ( + is_list_type, + is_union_type, + extract_type_arg, + is_iterable_type, + is_required_type, + is_annotated_type, + strip_annotated_type, +) +from .._compat import model_dump, is_typeddict + +_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) + + +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 `PropertInfo` 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 _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. + """ + if inner_type is None: + inner_type = annotation + + stripped_type = strip_annotated_type(inner_type) + if is_typeddict(stripped_type) and is_mapping(data): + return _transform_typeddict(data, stripped_type) + + 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)) + ): + inner_type = extract_type_arg(stripped_type, 0) + 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) + + 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(): + 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. + """ + if inner_type is None: + inner_type = annotation + + stripped_type = strip_annotated_type(inner_type) + if is_typeddict(stripped_type) and is_mapping(data): + return await _async_transform_typeddict(data, stripped_type) + + 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)) + ): + inner_type = extract_type_arg(stripped_type, 0) + 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) + + 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(): + 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 diff --git a/src/browserbase/_utils/_typing.py b/src/browserbase/_utils/_typing.py new file mode 100644 index 00000000..c036991f --- /dev/null +++ b/src/browserbase/_utils/_typing.py @@ -0,0 +1,120 @@ +from __future__ import annotations + +from typing import Any, TypeVar, Iterable, cast +from collections import abc as _c_abc +from typing_extensions import Required, Annotated, get_args, get_origin + +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_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 + + +# Extracts T from Annotated[T, ...] or from Required[Annotated[T, ...]] +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: + # 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/browserbase/_utils/_utils.py b/src/browserbase/_utils/_utils.py new file mode 100644 index 00000000..0bba17ca --- /dev/null +++ b/src/browserbase/_utils/_utils.py @@ -0,0 +1,397 @@ +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 typing_extensions import TypeGuard + +import sniffio + +from .._types import NotGiven, FileTypes, NotGivenOr, HeadersLike +from .._compat import parse_date as parse_date, parse_datetime as parse_datetime + +_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 isinstance(obj, NotGiven): + # 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_is_file_content(obj, key=flattened_key) + assert flattened_key is not None + 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: NotGivenOr[_T]) -> TypeGuard[_T]: + return not isinstance(obj, NotGiven) + + +# 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 it's 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] diff --git a/src/browserbase/_version.py b/src/browserbase/_version.py new file mode 100644 index 00000000..f8f37176 --- /dev/null +++ b/src/browserbase/_version.py @@ -0,0 +1,4 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +__title__ = "browserbase" +__version__ = "0.0.1-alpha.0" diff --git a/src/browserbase/lib/.keep b/src/browserbase/lib/.keep new file mode 100644 index 00000000..5e2c99fd --- /dev/null +++ b/src/browserbase/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/browserbase/py.typed b/src/browserbase/py.typed new file mode 100644 index 00000000..e69de29b diff --git a/src/browserbase/resources/__init__.py b/src/browserbase/resources/__init__.py new file mode 100644 index 00000000..73451a50 --- /dev/null +++ b/src/browserbase/resources/__init__.py @@ -0,0 +1,61 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from .contexts import ( + ContextsResource, + AsyncContextsResource, + ContextsResourceWithRawResponse, + AsyncContextsResourceWithRawResponse, + ContextsResourceWithStreamingResponse, + AsyncContextsResourceWithStreamingResponse, +) +from .projects import ( + ProjectsResource, + AsyncProjectsResource, + ProjectsResourceWithRawResponse, + AsyncProjectsResourceWithRawResponse, + ProjectsResourceWithStreamingResponse, + AsyncProjectsResourceWithStreamingResponse, +) +from .sessions import ( + SessionsResource, + AsyncSessionsResource, + SessionsResourceWithRawResponse, + AsyncSessionsResourceWithRawResponse, + SessionsResourceWithStreamingResponse, + AsyncSessionsResourceWithStreamingResponse, +) +from .extensions import ( + ExtensionsResource, + AsyncExtensionsResource, + ExtensionsResourceWithRawResponse, + AsyncExtensionsResourceWithRawResponse, + ExtensionsResourceWithStreamingResponse, + AsyncExtensionsResourceWithStreamingResponse, +) + +__all__ = [ + "ContextsResource", + "AsyncContextsResource", + "ContextsResourceWithRawResponse", + "AsyncContextsResourceWithRawResponse", + "ContextsResourceWithStreamingResponse", + "AsyncContextsResourceWithStreamingResponse", + "ExtensionsResource", + "AsyncExtensionsResource", + "ExtensionsResourceWithRawResponse", + "AsyncExtensionsResourceWithRawResponse", + "ExtensionsResourceWithStreamingResponse", + "AsyncExtensionsResourceWithStreamingResponse", + "ProjectsResource", + "AsyncProjectsResource", + "ProjectsResourceWithRawResponse", + "AsyncProjectsResourceWithRawResponse", + "ProjectsResourceWithStreamingResponse", + "AsyncProjectsResourceWithStreamingResponse", + "SessionsResource", + "AsyncSessionsResource", + "SessionsResourceWithRawResponse", + "AsyncSessionsResourceWithRawResponse", + "SessionsResourceWithStreamingResponse", + "AsyncSessionsResourceWithStreamingResponse", +] diff --git a/src/browserbase/resources/contexts.py b/src/browserbase/resources/contexts.py new file mode 100644 index 00000000..9d7ea73b --- /dev/null +++ b/src/browserbase/resources/contexts.py @@ -0,0 +1,332 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import httpx + +from ..types import context_create_params +from .._types import NOT_GIVEN, Body, Query, Headers, NotGiven +from .._utils import ( + maybe_transform, + 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.context import Context +from ..types.context_create_response import ContextCreateResponse +from ..types.context_update_response import ContextUpdateResponse + +__all__ = ["ContextsResource", "AsyncContextsResource"] + + +class ContextsResource(SyncAPIResource): + @cached_property + def with_raw_response(self) -> ContextsResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return the + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/stainless-sdks/browserbase-python#accessing-raw-response-data-eg-headers + """ + return ContextsResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> ContextsResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/stainless-sdks/browserbase-python#with_streaming_response + """ + return ContextsResourceWithStreamingResponse(self) + + def create( + self, + *, + project_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, + ) -> ContextCreateResponse: + """Create a Context + + Args: + project_id: The Project ID. + + Can be found in + [Settings](https://www.browserbase.com/settings). + + 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( + "/v1/contexts", + body=maybe_transform({"project_id": project_id}, context_create_params.ContextCreateParams), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=ContextCreateResponse, + ) + + def retrieve( + self, + 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, + ) -> Context: + """ + Context + + 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 id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return self._get( + f"/v1/contexts/{id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=Context, + ) + + def update( + self, + 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, + ) -> ContextUpdateResponse: + """ + Update Context + + 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 id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return self._put( + f"/v1/contexts/{id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=ContextUpdateResponse, + ) + + +class AsyncContextsResource(AsyncAPIResource): + @cached_property + def with_raw_response(self) -> AsyncContextsResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return the + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/stainless-sdks/browserbase-python#accessing-raw-response-data-eg-headers + """ + return AsyncContextsResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncContextsResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/stainless-sdks/browserbase-python#with_streaming_response + """ + return AsyncContextsResourceWithStreamingResponse(self) + + async def create( + self, + *, + project_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, + ) -> ContextCreateResponse: + """Create a Context + + Args: + project_id: The Project ID. + + Can be found in + [Settings](https://www.browserbase.com/settings). + + 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( + "/v1/contexts", + body=await async_maybe_transform({"project_id": project_id}, context_create_params.ContextCreateParams), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=ContextCreateResponse, + ) + + async def retrieve( + self, + 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, + ) -> Context: + """ + Context + + 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 id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return await self._get( + f"/v1/contexts/{id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=Context, + ) + + async def update( + self, + 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, + ) -> ContextUpdateResponse: + """ + Update Context + + 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 id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return await self._put( + f"/v1/contexts/{id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=ContextUpdateResponse, + ) + + +class ContextsResourceWithRawResponse: + def __init__(self, contexts: ContextsResource) -> None: + self._contexts = contexts + + self.create = to_raw_response_wrapper( + contexts.create, + ) + self.retrieve = to_raw_response_wrapper( + contexts.retrieve, + ) + self.update = to_raw_response_wrapper( + contexts.update, + ) + + +class AsyncContextsResourceWithRawResponse: + def __init__(self, contexts: AsyncContextsResource) -> None: + self._contexts = contexts + + self.create = async_to_raw_response_wrapper( + contexts.create, + ) + self.retrieve = async_to_raw_response_wrapper( + contexts.retrieve, + ) + self.update = async_to_raw_response_wrapper( + contexts.update, + ) + + +class ContextsResourceWithStreamingResponse: + def __init__(self, contexts: ContextsResource) -> None: + self._contexts = contexts + + self.create = to_streamed_response_wrapper( + contexts.create, + ) + self.retrieve = to_streamed_response_wrapper( + contexts.retrieve, + ) + self.update = to_streamed_response_wrapper( + contexts.update, + ) + + +class AsyncContextsResourceWithStreamingResponse: + def __init__(self, contexts: AsyncContextsResource) -> None: + self._contexts = contexts + + self.create = async_to_streamed_response_wrapper( + contexts.create, + ) + self.retrieve = async_to_streamed_response_wrapper( + contexts.retrieve, + ) + self.update = async_to_streamed_response_wrapper( + contexts.update, + ) diff --git a/src/browserbase/resources/extensions.py b/src/browserbase/resources/extensions.py new file mode 100644 index 00000000..199f3744 --- /dev/null +++ b/src/browserbase/resources/extensions.py @@ -0,0 +1,342 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Mapping, cast + +import httpx + +from ..types import extension_create_params +from .._types import NOT_GIVEN, Body, Query, Headers, NoneType, NotGiven, FileTypes +from .._utils import ( + extract_files, + maybe_transform, + deepcopy_minimal, + 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.extension import Extension + +__all__ = ["ExtensionsResource", "AsyncExtensionsResource"] + + +class ExtensionsResource(SyncAPIResource): + @cached_property + def with_raw_response(self) -> ExtensionsResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return the + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/stainless-sdks/browserbase-python#accessing-raw-response-data-eg-headers + """ + return ExtensionsResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> ExtensionsResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/stainless-sdks/browserbase-python#with_streaming_response + """ + return ExtensionsResourceWithStreamingResponse(self) + + def create( + self, + *, + file: FileTypes, + # 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, + ) -> Extension: + """ + Upload an Extension + + 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 + """ + body = deepcopy_minimal({"file": file}) + files = extract_files(cast(Mapping[str, object], body), paths=[["file"]]) + # It should be noted that the actual Content-Type header that will be + # sent to the server will contain a `boundary` parameter, e.g. + # multipart/form-data; boundary=---abc-- + extra_headers = {"Content-Type": "multipart/form-data", **(extra_headers or {})} + return self._post( + "/v1/extensions", + body=maybe_transform(body, extension_create_params.ExtensionCreateParams), + files=files, + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=Extension, + ) + + def retrieve( + self, + 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, + ) -> Extension: + """ + Extension + + 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 id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return self._get( + f"/v1/extensions/{id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=Extension, + ) + + def delete( + self, + 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, + ) -> None: + """ + Delete Extension + + 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 id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + extra_headers = {"Accept": "*/*", **(extra_headers or {})} + return self._delete( + f"/v1/extensions/{id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=NoneType, + ) + + +class AsyncExtensionsResource(AsyncAPIResource): + @cached_property + def with_raw_response(self) -> AsyncExtensionsResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return the + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/stainless-sdks/browserbase-python#accessing-raw-response-data-eg-headers + """ + return AsyncExtensionsResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncExtensionsResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/stainless-sdks/browserbase-python#with_streaming_response + """ + return AsyncExtensionsResourceWithStreamingResponse(self) + + async def create( + self, + *, + file: FileTypes, + # 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, + ) -> Extension: + """ + Upload an Extension + + 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 + """ + body = deepcopy_minimal({"file": file}) + files = extract_files(cast(Mapping[str, object], body), paths=[["file"]]) + # It should be noted that the actual Content-Type header that will be + # sent to the server will contain a `boundary` parameter, e.g. + # multipart/form-data; boundary=---abc-- + extra_headers = {"Content-Type": "multipart/form-data", **(extra_headers or {})} + return await self._post( + "/v1/extensions", + body=await async_maybe_transform(body, extension_create_params.ExtensionCreateParams), + files=files, + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=Extension, + ) + + async def retrieve( + self, + 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, + ) -> Extension: + """ + Extension + + 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 id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return await self._get( + f"/v1/extensions/{id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=Extension, + ) + + async def delete( + self, + 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, + ) -> None: + """ + Delete Extension + + 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 id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + extra_headers = {"Accept": "*/*", **(extra_headers or {})} + return await self._delete( + f"/v1/extensions/{id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=NoneType, + ) + + +class ExtensionsResourceWithRawResponse: + def __init__(self, extensions: ExtensionsResource) -> None: + self._extensions = extensions + + self.create = to_raw_response_wrapper( + extensions.create, + ) + self.retrieve = to_raw_response_wrapper( + extensions.retrieve, + ) + self.delete = to_raw_response_wrapper( + extensions.delete, + ) + + +class AsyncExtensionsResourceWithRawResponse: + def __init__(self, extensions: AsyncExtensionsResource) -> None: + self._extensions = extensions + + self.create = async_to_raw_response_wrapper( + extensions.create, + ) + self.retrieve = async_to_raw_response_wrapper( + extensions.retrieve, + ) + self.delete = async_to_raw_response_wrapper( + extensions.delete, + ) + + +class ExtensionsResourceWithStreamingResponse: + def __init__(self, extensions: ExtensionsResource) -> None: + self._extensions = extensions + + self.create = to_streamed_response_wrapper( + extensions.create, + ) + self.retrieve = to_streamed_response_wrapper( + extensions.retrieve, + ) + self.delete = to_streamed_response_wrapper( + extensions.delete, + ) + + +class AsyncExtensionsResourceWithStreamingResponse: + def __init__(self, extensions: AsyncExtensionsResource) -> None: + self._extensions = extensions + + self.create = async_to_streamed_response_wrapper( + extensions.create, + ) + self.retrieve = async_to_streamed_response_wrapper( + extensions.retrieve, + ) + self.delete = async_to_streamed_response_wrapper( + extensions.delete, + ) diff --git a/src/browserbase/resources/projects.py b/src/browserbase/resources/projects.py new file mode 100644 index 00000000..0d564b04 --- /dev/null +++ b/src/browserbase/resources/projects.py @@ -0,0 +1,293 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import httpx + +from .._types import NOT_GIVEN, Body, Query, Headers, NotGiven +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.project import Project +from ..types.project_usage import ProjectUsage +from ..types.project_list_response import ProjectListResponse + +__all__ = ["ProjectsResource", "AsyncProjectsResource"] + + +class ProjectsResource(SyncAPIResource): + @cached_property + def with_raw_response(self) -> ProjectsResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return the + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/stainless-sdks/browserbase-python#accessing-raw-response-data-eg-headers + """ + return ProjectsResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> ProjectsResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/stainless-sdks/browserbase-python#with_streaming_response + """ + return ProjectsResourceWithStreamingResponse(self) + + def retrieve( + self, + 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, + ) -> Project: + """ + Project + + 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 id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return self._get( + f"/v1/projects/{id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=Project, + ) + + def list( + self, + *, + # 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, + ) -> ProjectListResponse: + """List all projects""" + return self._get( + "/v1/projects", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=ProjectListResponse, + ) + + def usage( + self, + 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, + ) -> ProjectUsage: + """ + Project Usage + + 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 id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return self._get( + f"/v1/projects/{id}/usage", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=ProjectUsage, + ) + + +class AsyncProjectsResource(AsyncAPIResource): + @cached_property + def with_raw_response(self) -> AsyncProjectsResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return the + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/stainless-sdks/browserbase-python#accessing-raw-response-data-eg-headers + """ + return AsyncProjectsResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncProjectsResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/stainless-sdks/browserbase-python#with_streaming_response + """ + return AsyncProjectsResourceWithStreamingResponse(self) + + async def retrieve( + self, + 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, + ) -> Project: + """ + Project + + 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 id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return await self._get( + f"/v1/projects/{id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=Project, + ) + + async def list( + self, + *, + # 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, + ) -> ProjectListResponse: + """List all projects""" + return await self._get( + "/v1/projects", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=ProjectListResponse, + ) + + async def usage( + self, + 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, + ) -> ProjectUsage: + """ + Project Usage + + 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 id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return await self._get( + f"/v1/projects/{id}/usage", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=ProjectUsage, + ) + + +class ProjectsResourceWithRawResponse: + def __init__(self, projects: ProjectsResource) -> None: + self._projects = projects + + self.retrieve = to_raw_response_wrapper( + projects.retrieve, + ) + self.list = to_raw_response_wrapper( + projects.list, + ) + self.usage = to_raw_response_wrapper( + projects.usage, + ) + + +class AsyncProjectsResourceWithRawResponse: + def __init__(self, projects: AsyncProjectsResource) -> None: + self._projects = projects + + self.retrieve = async_to_raw_response_wrapper( + projects.retrieve, + ) + self.list = async_to_raw_response_wrapper( + projects.list, + ) + self.usage = async_to_raw_response_wrapper( + projects.usage, + ) + + +class ProjectsResourceWithStreamingResponse: + def __init__(self, projects: ProjectsResource) -> None: + self._projects = projects + + self.retrieve = to_streamed_response_wrapper( + projects.retrieve, + ) + self.list = to_streamed_response_wrapper( + projects.list, + ) + self.usage = to_streamed_response_wrapper( + projects.usage, + ) + + +class AsyncProjectsResourceWithStreamingResponse: + def __init__(self, projects: AsyncProjectsResource) -> None: + self._projects = projects + + self.retrieve = async_to_streamed_response_wrapper( + projects.retrieve, + ) + self.list = async_to_streamed_response_wrapper( + projects.list, + ) + self.usage = async_to_streamed_response_wrapper( + projects.usage, + ) diff --git a/src/browserbase/resources/sessions/__init__.py b/src/browserbase/resources/sessions/__init__.py new file mode 100644 index 00000000..b3877e12 --- /dev/null +++ b/src/browserbase/resources/sessions/__init__.py @@ -0,0 +1,75 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from .logs import ( + LogsResource, + AsyncLogsResource, + LogsResourceWithRawResponse, + AsyncLogsResourceWithRawResponse, + LogsResourceWithStreamingResponse, + AsyncLogsResourceWithStreamingResponse, +) +from .uploads import ( + UploadsResource, + AsyncUploadsResource, + UploadsResourceWithRawResponse, + AsyncUploadsResourceWithRawResponse, + UploadsResourceWithStreamingResponse, + AsyncUploadsResourceWithStreamingResponse, +) +from .sessions import ( + SessionsResource, + AsyncSessionsResource, + SessionsResourceWithRawResponse, + AsyncSessionsResourceWithRawResponse, + SessionsResourceWithStreamingResponse, + AsyncSessionsResourceWithStreamingResponse, +) +from .downloads import ( + DownloadsResource, + AsyncDownloadsResource, + DownloadsResourceWithRawResponse, + AsyncDownloadsResourceWithRawResponse, + DownloadsResourceWithStreamingResponse, + AsyncDownloadsResourceWithStreamingResponse, +) +from .recording import ( + RecordingResource, + AsyncRecordingResource, + RecordingResourceWithRawResponse, + AsyncRecordingResourceWithRawResponse, + RecordingResourceWithStreamingResponse, + AsyncRecordingResourceWithStreamingResponse, +) + +__all__ = [ + "DownloadsResource", + "AsyncDownloadsResource", + "DownloadsResourceWithRawResponse", + "AsyncDownloadsResourceWithRawResponse", + "DownloadsResourceWithStreamingResponse", + "AsyncDownloadsResourceWithStreamingResponse", + "LogsResource", + "AsyncLogsResource", + "LogsResourceWithRawResponse", + "AsyncLogsResourceWithRawResponse", + "LogsResourceWithStreamingResponse", + "AsyncLogsResourceWithStreamingResponse", + "RecordingResource", + "AsyncRecordingResource", + "RecordingResourceWithRawResponse", + "AsyncRecordingResourceWithRawResponse", + "RecordingResourceWithStreamingResponse", + "AsyncRecordingResourceWithStreamingResponse", + "UploadsResource", + "AsyncUploadsResource", + "UploadsResourceWithRawResponse", + "AsyncUploadsResourceWithRawResponse", + "UploadsResourceWithStreamingResponse", + "AsyncUploadsResourceWithStreamingResponse", + "SessionsResource", + "AsyncSessionsResource", + "SessionsResourceWithRawResponse", + "AsyncSessionsResourceWithRawResponse", + "SessionsResourceWithStreamingResponse", + "AsyncSessionsResourceWithStreamingResponse", +] diff --git a/src/browserbase/resources/sessions/downloads.py b/src/browserbase/resources/sessions/downloads.py new file mode 100644 index 00000000..1792aecc --- /dev/null +++ b/src/browserbase/resources/sessions/downloads.py @@ -0,0 +1,172 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import httpx + +from ..._types import NOT_GIVEN, Body, Query, Headers, NotGiven +from ..._compat import cached_property +from ..._resource import SyncAPIResource, AsyncAPIResource +from ..._response import ( + BinaryAPIResponse, + AsyncBinaryAPIResponse, + StreamedBinaryAPIResponse, + AsyncStreamedBinaryAPIResponse, + to_custom_raw_response_wrapper, + to_custom_streamed_response_wrapper, + async_to_custom_raw_response_wrapper, + async_to_custom_streamed_response_wrapper, +) +from ..._base_client import make_request_options + +__all__ = ["DownloadsResource", "AsyncDownloadsResource"] + + +class DownloadsResource(SyncAPIResource): + @cached_property + def with_raw_response(self) -> DownloadsResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return the + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/stainless-sdks/browserbase-python#accessing-raw-response-data-eg-headers + """ + return DownloadsResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> DownloadsResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/stainless-sdks/browserbase-python#with_streaming_response + """ + return DownloadsResourceWithStreamingResponse(self) + + def list( + self, + 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, + ) -> BinaryAPIResponse: + """ + Session Downloads + + 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 id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + extra_headers = {"Accept": "application/octet-stream", **(extra_headers or {})} + return self._get( + f"/v1/sessions/{id}/downloads", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=BinaryAPIResponse, + ) + + +class AsyncDownloadsResource(AsyncAPIResource): + @cached_property + def with_raw_response(self) -> AsyncDownloadsResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return the + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/stainless-sdks/browserbase-python#accessing-raw-response-data-eg-headers + """ + return AsyncDownloadsResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncDownloadsResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/stainless-sdks/browserbase-python#with_streaming_response + """ + return AsyncDownloadsResourceWithStreamingResponse(self) + + async def list( + self, + 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, + ) -> AsyncBinaryAPIResponse: + """ + Session Downloads + + 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 id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + extra_headers = {"Accept": "application/octet-stream", **(extra_headers or {})} + return await self._get( + f"/v1/sessions/{id}/downloads", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=AsyncBinaryAPIResponse, + ) + + +class DownloadsResourceWithRawResponse: + def __init__(self, downloads: DownloadsResource) -> None: + self._downloads = downloads + + self.list = to_custom_raw_response_wrapper( + downloads.list, + BinaryAPIResponse, + ) + + +class AsyncDownloadsResourceWithRawResponse: + def __init__(self, downloads: AsyncDownloadsResource) -> None: + self._downloads = downloads + + self.list = async_to_custom_raw_response_wrapper( + downloads.list, + AsyncBinaryAPIResponse, + ) + + +class DownloadsResourceWithStreamingResponse: + def __init__(self, downloads: DownloadsResource) -> None: + self._downloads = downloads + + self.list = to_custom_streamed_response_wrapper( + downloads.list, + StreamedBinaryAPIResponse, + ) + + +class AsyncDownloadsResourceWithStreamingResponse: + def __init__(self, downloads: AsyncDownloadsResource) -> None: + self._downloads = downloads + + self.list = async_to_custom_streamed_response_wrapper( + downloads.list, + AsyncStreamedBinaryAPIResponse, + ) diff --git a/src/browserbase/resources/sessions/logs.py b/src/browserbase/resources/sessions/logs.py new file mode 100644 index 00000000..3ab61905 --- /dev/null +++ b/src/browserbase/resources/sessions/logs.py @@ -0,0 +1,163 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import httpx + +from ..._types import NOT_GIVEN, Body, Query, Headers, NotGiven +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.sessions.log_list_response import LogListResponse + +__all__ = ["LogsResource", "AsyncLogsResource"] + + +class LogsResource(SyncAPIResource): + @cached_property + def with_raw_response(self) -> LogsResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return the + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/stainless-sdks/browserbase-python#accessing-raw-response-data-eg-headers + """ + return LogsResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> LogsResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/stainless-sdks/browserbase-python#with_streaming_response + """ + return LogsResourceWithStreamingResponse(self) + + def list( + self, + 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, + ) -> LogListResponse: + """ + Session Logs + + 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 id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return self._get( + f"/v1/sessions/{id}/logs", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=LogListResponse, + ) + + +class AsyncLogsResource(AsyncAPIResource): + @cached_property + def with_raw_response(self) -> AsyncLogsResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return the + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/stainless-sdks/browserbase-python#accessing-raw-response-data-eg-headers + """ + return AsyncLogsResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncLogsResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/stainless-sdks/browserbase-python#with_streaming_response + """ + return AsyncLogsResourceWithStreamingResponse(self) + + async def list( + self, + 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, + ) -> LogListResponse: + """ + Session Logs + + 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 id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return await self._get( + f"/v1/sessions/{id}/logs", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=LogListResponse, + ) + + +class LogsResourceWithRawResponse: + def __init__(self, logs: LogsResource) -> None: + self._logs = logs + + self.list = to_raw_response_wrapper( + logs.list, + ) + + +class AsyncLogsResourceWithRawResponse: + def __init__(self, logs: AsyncLogsResource) -> None: + self._logs = logs + + self.list = async_to_raw_response_wrapper( + logs.list, + ) + + +class LogsResourceWithStreamingResponse: + def __init__(self, logs: LogsResource) -> None: + self._logs = logs + + self.list = to_streamed_response_wrapper( + logs.list, + ) + + +class AsyncLogsResourceWithStreamingResponse: + def __init__(self, logs: AsyncLogsResource) -> None: + self._logs = logs + + self.list = async_to_streamed_response_wrapper( + logs.list, + ) diff --git a/src/browserbase/resources/sessions/recording.py b/src/browserbase/resources/sessions/recording.py new file mode 100644 index 00000000..2904f527 --- /dev/null +++ b/src/browserbase/resources/sessions/recording.py @@ -0,0 +1,163 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import httpx + +from ..._types import NOT_GIVEN, Body, Query, Headers, NotGiven +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.sessions.recording_retrieve_response import RecordingRetrieveResponse + +__all__ = ["RecordingResource", "AsyncRecordingResource"] + + +class RecordingResource(SyncAPIResource): + @cached_property + def with_raw_response(self) -> RecordingResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return the + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/stainless-sdks/browserbase-python#accessing-raw-response-data-eg-headers + """ + return RecordingResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> RecordingResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/stainless-sdks/browserbase-python#with_streaming_response + """ + return RecordingResourceWithStreamingResponse(self) + + def retrieve( + self, + 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, + ) -> RecordingRetrieveResponse: + """ + Session Recording + + 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 id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return self._get( + f"/v1/sessions/{id}/recording", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=RecordingRetrieveResponse, + ) + + +class AsyncRecordingResource(AsyncAPIResource): + @cached_property + def with_raw_response(self) -> AsyncRecordingResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return the + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/stainless-sdks/browserbase-python#accessing-raw-response-data-eg-headers + """ + return AsyncRecordingResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncRecordingResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/stainless-sdks/browserbase-python#with_streaming_response + """ + return AsyncRecordingResourceWithStreamingResponse(self) + + async def retrieve( + self, + 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, + ) -> RecordingRetrieveResponse: + """ + Session Recording + + 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 id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return await self._get( + f"/v1/sessions/{id}/recording", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=RecordingRetrieveResponse, + ) + + +class RecordingResourceWithRawResponse: + def __init__(self, recording: RecordingResource) -> None: + self._recording = recording + + self.retrieve = to_raw_response_wrapper( + recording.retrieve, + ) + + +class AsyncRecordingResourceWithRawResponse: + def __init__(self, recording: AsyncRecordingResource) -> None: + self._recording = recording + + self.retrieve = async_to_raw_response_wrapper( + recording.retrieve, + ) + + +class RecordingResourceWithStreamingResponse: + def __init__(self, recording: RecordingResource) -> None: + self._recording = recording + + self.retrieve = to_streamed_response_wrapper( + recording.retrieve, + ) + + +class AsyncRecordingResourceWithStreamingResponse: + def __init__(self, recording: AsyncRecordingResource) -> None: + self._recording = recording + + self.retrieve = async_to_streamed_response_wrapper( + recording.retrieve, + ) diff --git a/src/browserbase/resources/sessions/sessions.py b/src/browserbase/resources/sessions/sessions.py new file mode 100644 index 00000000..6f6e10d1 --- /dev/null +++ b/src/browserbase/resources/sessions/sessions.py @@ -0,0 +1,709 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Union, Iterable +from typing_extensions import Literal + +import httpx + +from .logs import ( + LogsResource, + AsyncLogsResource, + LogsResourceWithRawResponse, + AsyncLogsResourceWithRawResponse, + LogsResourceWithStreamingResponse, + AsyncLogsResourceWithStreamingResponse, +) +from ...types import session_list_params, session_create_params, session_update_params +from .uploads import ( + UploadsResource, + AsyncUploadsResource, + UploadsResourceWithRawResponse, + AsyncUploadsResourceWithRawResponse, + UploadsResourceWithStreamingResponse, + AsyncUploadsResourceWithStreamingResponse, +) +from ..._types import NOT_GIVEN, Body, Query, Headers, NotGiven +from ..._utils import ( + maybe_transform, + async_maybe_transform, +) +from ..._compat import cached_property +from .downloads import ( + DownloadsResource, + AsyncDownloadsResource, + DownloadsResourceWithRawResponse, + AsyncDownloadsResourceWithRawResponse, + DownloadsResourceWithStreamingResponse, + AsyncDownloadsResourceWithStreamingResponse, +) +from .recording import ( + RecordingResource, + AsyncRecordingResource, + RecordingResourceWithRawResponse, + AsyncRecordingResourceWithRawResponse, + RecordingResourceWithStreamingResponse, + AsyncRecordingResourceWithStreamingResponse, +) +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 import Session +from ...types.session_live_urls import SessionLiveURLs +from ...types.session_list_response import SessionListResponse + +__all__ = ["SessionsResource", "AsyncSessionsResource"] + + +class SessionsResource(SyncAPIResource): + @cached_property + def downloads(self) -> DownloadsResource: + return DownloadsResource(self._client) + + @cached_property + def logs(self) -> LogsResource: + return LogsResource(self._client) + + @cached_property + def recording(self) -> RecordingResource: + return RecordingResource(self._client) + + @cached_property + def uploads(self) -> UploadsResource: + return UploadsResource(self._client) + + @cached_property + def with_raw_response(self) -> SessionsResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return the + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/stainless-sdks/browserbase-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/browserbase-python#with_streaming_response + """ + return SessionsResourceWithStreamingResponse(self) + + def create( + self, + *, + project_id: str, + browser_settings: session_create_params.BrowserSettings | NotGiven = NOT_GIVEN, + extension_id: str | NotGiven = NOT_GIVEN, + keep_alive: bool | NotGiven = NOT_GIVEN, + proxies: Union[bool, Iterable[session_create_params.ProxiesUnionMember1]] | NotGiven = NOT_GIVEN, + api_timeout: int | NotGiven = NOT_GIVEN, + # 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, + ) -> Session: + """Create a Session + + Args: + project_id: The Project ID. + + Can be found in + [Settings](https://www.browserbase.com/settings). + + extension_id: The uploaded Extension ID. See + [Upload Extension](/reference/api/upload-an-extension). + + keep_alive: Set to true to keep the session alive even after disconnections. This is + available on the Startup plan only. + + proxies: Proxy configuration. Can be true for default proxy, or an array of proxy + configurations. + + api_timeout: Duration in seconds after which the session will automatically end. Defaults to + the Project's `defaultTimeout`. + + 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( + "/v1/sessions", + body=maybe_transform( + { + "project_id": project_id, + "browser_settings": browser_settings, + "extension_id": extension_id, + "keep_alive": keep_alive, + "proxies": proxies, + "timeout": api_timeout, + }, + session_create_params.SessionCreateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=Session, + ) + + def retrieve( + self, + 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, + ) -> Session: + """ + 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 id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return self._get( + f"/v1/sessions/{id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=Session, + ) + + def update( + self, + id: str, + *, + project_id: str, + status: Literal["REQUEST_RELEASE"], + # 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, + ) -> Session: + """Update Session + + Args: + project_id: The Project ID. + + Can be found in + [Settings](https://www.browserbase.com/settings). + + status: Set to `REQUEST_RELEASE` to request that the session complete. Use before + session's timeout to avoid additional charges. + + 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}") + return self._post( + f"/v1/sessions/{id}", + body=maybe_transform( + { + "project_id": project_id, + "status": status, + }, + session_update_params.SessionUpdateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=Session, + ) + + def list( + self, + *, + status: Literal["RUNNING", "ERROR", "TIMED_OUT", "COMPLETED"] | NotGiven = NOT_GIVEN, + # 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, + ) -> SessionListResponse: + """ + List Sessions + + 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 + """ + return self._get( + "/v1/sessions", + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform({"status": status}, session_list_params.SessionListParams), + ), + cast_to=SessionListResponse, + ) + + def debug( + self, + 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, + ) -> SessionLiveURLs: + """ + Session Live URLs + + 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 id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return self._get( + f"/v1/sessions/{id}/debug", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=SessionLiveURLs, + ) + + +class AsyncSessionsResource(AsyncAPIResource): + @cached_property + def downloads(self) -> AsyncDownloadsResource: + return AsyncDownloadsResource(self._client) + + @cached_property + def logs(self) -> AsyncLogsResource: + return AsyncLogsResource(self._client) + + @cached_property + def recording(self) -> AsyncRecordingResource: + return AsyncRecordingResource(self._client) + + @cached_property + def uploads(self) -> AsyncUploadsResource: + return AsyncUploadsResource(self._client) + + @cached_property + def with_raw_response(self) -> AsyncSessionsResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return the + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/stainless-sdks/browserbase-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/browserbase-python#with_streaming_response + """ + return AsyncSessionsResourceWithStreamingResponse(self) + + async def create( + self, + *, + project_id: str, + browser_settings: session_create_params.BrowserSettings | NotGiven = NOT_GIVEN, + extension_id: str | NotGiven = NOT_GIVEN, + keep_alive: bool | NotGiven = NOT_GIVEN, + proxies: Union[bool, Iterable[session_create_params.ProxiesUnionMember1]] | NotGiven = NOT_GIVEN, + api_timeout: int | NotGiven = NOT_GIVEN, + # 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, + ) -> Session: + """Create a Session + + Args: + project_id: The Project ID. + + Can be found in + [Settings](https://www.browserbase.com/settings). + + extension_id: The uploaded Extension ID. See + [Upload Extension](/reference/api/upload-an-extension). + + keep_alive: Set to true to keep the session alive even after disconnections. This is + available on the Startup plan only. + + proxies: Proxy configuration. Can be true for default proxy, or an array of proxy + configurations. + + api_timeout: Duration in seconds after which the session will automatically end. Defaults to + the Project's `defaultTimeout`. + + 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( + "/v1/sessions", + body=await async_maybe_transform( + { + "project_id": project_id, + "browser_settings": browser_settings, + "extension_id": extension_id, + "keep_alive": keep_alive, + "proxies": proxies, + "timeout": api_timeout, + }, + session_create_params.SessionCreateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=Session, + ) + + async def retrieve( + self, + 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, + ) -> Session: + """ + 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 id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return await self._get( + f"/v1/sessions/{id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=Session, + ) + + async def update( + self, + id: str, + *, + project_id: str, + status: Literal["REQUEST_RELEASE"], + # 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, + ) -> Session: + """Update Session + + Args: + project_id: The Project ID. + + Can be found in + [Settings](https://www.browserbase.com/settings). + + status: Set to `REQUEST_RELEASE` to request that the session complete. Use before + session's timeout to avoid additional charges. + + 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}") + return await self._post( + f"/v1/sessions/{id}", + body=await async_maybe_transform( + { + "project_id": project_id, + "status": status, + }, + session_update_params.SessionUpdateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=Session, + ) + + async def list( + self, + *, + status: Literal["RUNNING", "ERROR", "TIMED_OUT", "COMPLETED"] | NotGiven = NOT_GIVEN, + # 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, + ) -> SessionListResponse: + """ + List Sessions + + 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 + """ + return await self._get( + "/v1/sessions", + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=await async_maybe_transform({"status": status}, session_list_params.SessionListParams), + ), + cast_to=SessionListResponse, + ) + + async def debug( + self, + 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, + ) -> SessionLiveURLs: + """ + Session Live URLs + + 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 id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return await self._get( + f"/v1/sessions/{id}/debug", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=SessionLiveURLs, + ) + + +class SessionsResourceWithRawResponse: + def __init__(self, sessions: SessionsResource) -> None: + self._sessions = sessions + + self.create = to_raw_response_wrapper( + sessions.create, + ) + self.retrieve = to_raw_response_wrapper( + sessions.retrieve, + ) + self.update = to_raw_response_wrapper( + sessions.update, + ) + self.list = to_raw_response_wrapper( + sessions.list, + ) + self.debug = to_raw_response_wrapper( + sessions.debug, + ) + + @cached_property + def downloads(self) -> DownloadsResourceWithRawResponse: + return DownloadsResourceWithRawResponse(self._sessions.downloads) + + @cached_property + def logs(self) -> LogsResourceWithRawResponse: + return LogsResourceWithRawResponse(self._sessions.logs) + + @cached_property + def recording(self) -> RecordingResourceWithRawResponse: + return RecordingResourceWithRawResponse(self._sessions.recording) + + @cached_property + def uploads(self) -> UploadsResourceWithRawResponse: + return UploadsResourceWithRawResponse(self._sessions.uploads) + + +class AsyncSessionsResourceWithRawResponse: + def __init__(self, sessions: AsyncSessionsResource) -> None: + self._sessions = sessions + + self.create = async_to_raw_response_wrapper( + sessions.create, + ) + self.retrieve = async_to_raw_response_wrapper( + sessions.retrieve, + ) + self.update = async_to_raw_response_wrapper( + sessions.update, + ) + self.list = async_to_raw_response_wrapper( + sessions.list, + ) + self.debug = async_to_raw_response_wrapper( + sessions.debug, + ) + + @cached_property + def downloads(self) -> AsyncDownloadsResourceWithRawResponse: + return AsyncDownloadsResourceWithRawResponse(self._sessions.downloads) + + @cached_property + def logs(self) -> AsyncLogsResourceWithRawResponse: + return AsyncLogsResourceWithRawResponse(self._sessions.logs) + + @cached_property + def recording(self) -> AsyncRecordingResourceWithRawResponse: + return AsyncRecordingResourceWithRawResponse(self._sessions.recording) + + @cached_property + def uploads(self) -> AsyncUploadsResourceWithRawResponse: + return AsyncUploadsResourceWithRawResponse(self._sessions.uploads) + + +class SessionsResourceWithStreamingResponse: + def __init__(self, sessions: SessionsResource) -> None: + self._sessions = sessions + + self.create = to_streamed_response_wrapper( + sessions.create, + ) + self.retrieve = to_streamed_response_wrapper( + sessions.retrieve, + ) + self.update = to_streamed_response_wrapper( + sessions.update, + ) + self.list = to_streamed_response_wrapper( + sessions.list, + ) + self.debug = to_streamed_response_wrapper( + sessions.debug, + ) + + @cached_property + def downloads(self) -> DownloadsResourceWithStreamingResponse: + return DownloadsResourceWithStreamingResponse(self._sessions.downloads) + + @cached_property + def logs(self) -> LogsResourceWithStreamingResponse: + return LogsResourceWithStreamingResponse(self._sessions.logs) + + @cached_property + def recording(self) -> RecordingResourceWithStreamingResponse: + return RecordingResourceWithStreamingResponse(self._sessions.recording) + + @cached_property + def uploads(self) -> UploadsResourceWithStreamingResponse: + return UploadsResourceWithStreamingResponse(self._sessions.uploads) + + +class AsyncSessionsResourceWithStreamingResponse: + def __init__(self, sessions: AsyncSessionsResource) -> None: + self._sessions = sessions + + self.create = async_to_streamed_response_wrapper( + sessions.create, + ) + self.retrieve = async_to_streamed_response_wrapper( + sessions.retrieve, + ) + self.update = async_to_streamed_response_wrapper( + sessions.update, + ) + self.list = async_to_streamed_response_wrapper( + sessions.list, + ) + self.debug = async_to_streamed_response_wrapper( + sessions.debug, + ) + + @cached_property + def downloads(self) -> AsyncDownloadsResourceWithStreamingResponse: + return AsyncDownloadsResourceWithStreamingResponse(self._sessions.downloads) + + @cached_property + def logs(self) -> AsyncLogsResourceWithStreamingResponse: + return AsyncLogsResourceWithStreamingResponse(self._sessions.logs) + + @cached_property + def recording(self) -> AsyncRecordingResourceWithStreamingResponse: + return AsyncRecordingResourceWithStreamingResponse(self._sessions.recording) + + @cached_property + def uploads(self) -> AsyncUploadsResourceWithStreamingResponse: + return AsyncUploadsResourceWithStreamingResponse(self._sessions.uploads) diff --git a/src/browserbase/resources/sessions/uploads.py b/src/browserbase/resources/sessions/uploads.py new file mode 100644 index 00000000..f55820ba --- /dev/null +++ b/src/browserbase/resources/sessions/uploads.py @@ -0,0 +1,190 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Mapping, cast + +import httpx + +from ..._types import NOT_GIVEN, Body, Query, Headers, NotGiven, FileTypes +from ..._utils import ( + extract_files, + maybe_transform, + deepcopy_minimal, + 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.sessions import upload_create_params +from ...types.sessions.upload_create_response import UploadCreateResponse + +__all__ = ["UploadsResource", "AsyncUploadsResource"] + + +class UploadsResource(SyncAPIResource): + @cached_property + def with_raw_response(self) -> UploadsResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return the + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/stainless-sdks/browserbase-python#accessing-raw-response-data-eg-headers + """ + return UploadsResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> UploadsResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/stainless-sdks/browserbase-python#with_streaming_response + """ + return UploadsResourceWithStreamingResponse(self) + + def create( + self, + id: str, + *, + file: FileTypes, + # 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, + ) -> UploadCreateResponse: + """ + Create Session Uploads + + 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 id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + body = deepcopy_minimal({"file": file}) + files = extract_files(cast(Mapping[str, object], body), paths=[["file"]]) + # It should be noted that the actual Content-Type header that will be + # sent to the server will contain a `boundary` parameter, e.g. + # multipart/form-data; boundary=---abc-- + extra_headers = {"Content-Type": "multipart/form-data", **(extra_headers or {})} + return self._post( + f"/v1/sessions/{id}/uploads", + body=maybe_transform(body, upload_create_params.UploadCreateParams), + files=files, + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=UploadCreateResponse, + ) + + +class AsyncUploadsResource(AsyncAPIResource): + @cached_property + def with_raw_response(self) -> AsyncUploadsResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return the + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/stainless-sdks/browserbase-python#accessing-raw-response-data-eg-headers + """ + return AsyncUploadsResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncUploadsResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/stainless-sdks/browserbase-python#with_streaming_response + """ + return AsyncUploadsResourceWithStreamingResponse(self) + + async def create( + self, + id: str, + *, + file: FileTypes, + # 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, + ) -> UploadCreateResponse: + """ + Create Session Uploads + + 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 id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + body = deepcopy_minimal({"file": file}) + files = extract_files(cast(Mapping[str, object], body), paths=[["file"]]) + # It should be noted that the actual Content-Type header that will be + # sent to the server will contain a `boundary` parameter, e.g. + # multipart/form-data; boundary=---abc-- + extra_headers = {"Content-Type": "multipart/form-data", **(extra_headers or {})} + return await self._post( + f"/v1/sessions/{id}/uploads", + body=await async_maybe_transform(body, upload_create_params.UploadCreateParams), + files=files, + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=UploadCreateResponse, + ) + + +class UploadsResourceWithRawResponse: + def __init__(self, uploads: UploadsResource) -> None: + self._uploads = uploads + + self.create = to_raw_response_wrapper( + uploads.create, + ) + + +class AsyncUploadsResourceWithRawResponse: + def __init__(self, uploads: AsyncUploadsResource) -> None: + self._uploads = uploads + + self.create = async_to_raw_response_wrapper( + uploads.create, + ) + + +class UploadsResourceWithStreamingResponse: + def __init__(self, uploads: UploadsResource) -> None: + self._uploads = uploads + + self.create = to_streamed_response_wrapper( + uploads.create, + ) + + +class AsyncUploadsResourceWithStreamingResponse: + def __init__(self, uploads: AsyncUploadsResource) -> None: + self._uploads = uploads + + self.create = async_to_streamed_response_wrapper( + uploads.create, + ) diff --git a/src/browserbase/types/__init__.py b/src/browserbase/types/__init__.py new file mode 100644 index 00000000..fcbbf0a0 --- /dev/null +++ b/src/browserbase/types/__init__.py @@ -0,0 +1,19 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from .context import Context as Context +from .project import Project as Project +from .session import Session as Session +from .extension import Extension as Extension +from .project_usage import ProjectUsage as ProjectUsage +from .session_live_urls import SessionLiveURLs as SessionLiveURLs +from .session_list_params import SessionListParams as SessionListParams +from .context_create_params import ContextCreateParams as ContextCreateParams +from .project_list_response import ProjectListResponse as ProjectListResponse +from .session_create_params import SessionCreateParams as SessionCreateParams +from .session_list_response import SessionListResponse as SessionListResponse +from .session_update_params import SessionUpdateParams as SessionUpdateParams +from .context_create_response import ContextCreateResponse as ContextCreateResponse +from .context_update_response import ContextUpdateResponse as ContextUpdateResponse +from .extension_create_params import ExtensionCreateParams as ExtensionCreateParams diff --git a/src/browserbase/types/context.py b/src/browserbase/types/context.py new file mode 100644 index 00000000..ed5135b5 --- /dev/null +++ b/src/browserbase/types/context.py @@ -0,0 +1,17 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from datetime import datetime + +from pydantic import Field as FieldInfo + +from .._models import BaseModel + +__all__ = ["Context"] + + +class Context(BaseModel): + id: str + + created_at: datetime = FieldInfo(alias="createdAt") + + updated_at: datetime = FieldInfo(alias="updatedAt") diff --git a/src/browserbase/types/context_create_params.py b/src/browserbase/types/context_create_params.py new file mode 100644 index 00000000..75cd1fcd --- /dev/null +++ b/src/browserbase/types/context_create_params.py @@ -0,0 +1,17 @@ +# 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 .._utils import PropertyInfo + +__all__ = ["ContextCreateParams"] + + +class ContextCreateParams(TypedDict, total=False): + project_id: Required[Annotated[str, PropertyInfo(alias="projectId")]] + """The Project ID. + + Can be found in [Settings](https://www.browserbase.com/settings). + """ diff --git a/src/browserbase/types/context_create_response.py b/src/browserbase/types/context_create_response.py new file mode 100644 index 00000000..c168596e --- /dev/null +++ b/src/browserbase/types/context_create_response.py @@ -0,0 +1,30 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + + +from pydantic import Field as FieldInfo + +from .._models import BaseModel + +__all__ = ["ContextCreateResponse"] + + +class ContextCreateResponse(BaseModel): + id: str + + cipher_algorithm: str = FieldInfo(alias="cipherAlgorithm") + """The cipher algorithm used to encrypt the user-data-directory. + + AES-256-CBC is currently the only supported algorithm. + """ + + initialization_vector_size: int = FieldInfo(alias="initializationVectorSize") + """The initialization vector size used to encrypt the user-data-directory. + + [Read more about how to use it](/features/contexts). + """ + + public_key: str = FieldInfo(alias="publicKey") + """The public key to encrypt the user-data-directory.""" + + upload_url: str = FieldInfo(alias="uploadUrl") + """An upload URL to upload a custom user-data-directory.""" diff --git a/src/browserbase/types/context_update_response.py b/src/browserbase/types/context_update_response.py new file mode 100644 index 00000000..d07e50e7 --- /dev/null +++ b/src/browserbase/types/context_update_response.py @@ -0,0 +1,30 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + + +from pydantic import Field as FieldInfo + +from .._models import BaseModel + +__all__ = ["ContextUpdateResponse"] + + +class ContextUpdateResponse(BaseModel): + id: str + + cipher_algorithm: str = FieldInfo(alias="cipherAlgorithm") + """The cipher algorithm used to encrypt the user-data-directory. + + AES-256-CBC is currently the only supported algorithm. + """ + + initialization_vector_size: int = FieldInfo(alias="initializationVectorSize") + """The initialization vector size used to encrypt the user-data-directory. + + [Read more about how to use it](/features/contexts). + """ + + public_key: str = FieldInfo(alias="publicKey") + """The public key to encrypt the user-data-directory.""" + + upload_url: str = FieldInfo(alias="uploadUrl") + """An upload URL to upload a custom user-data-directory.""" diff --git a/src/browserbase/types/extension.py b/src/browserbase/types/extension.py new file mode 100644 index 00000000..da1a8b5c --- /dev/null +++ b/src/browserbase/types/extension.py @@ -0,0 +1,17 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from datetime import datetime + +from pydantic import Field as FieldInfo + +from .._models import BaseModel + +__all__ = ["Extension"] + + +class Extension(BaseModel): + id: str + + created_at: datetime = FieldInfo(alias="createdAt") + + updated_at: datetime = FieldInfo(alias="updatedAt") diff --git a/src/browserbase/types/extension_create_params.py b/src/browserbase/types/extension_create_params.py new file mode 100644 index 00000000..730cec77 --- /dev/null +++ b/src/browserbase/types/extension_create_params.py @@ -0,0 +1,13 @@ +# 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 FileTypes + +__all__ = ["ExtensionCreateParams"] + + +class ExtensionCreateParams(TypedDict, total=False): + file: Required[FileTypes] diff --git a/src/browserbase/types/project.py b/src/browserbase/types/project.py new file mode 100644 index 00000000..cba2873f --- /dev/null +++ b/src/browserbase/types/project.py @@ -0,0 +1,17 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from datetime import datetime + +from pydantic import Field as FieldInfo + +from .._models import BaseModel + +__all__ = ["Project"] + + +class Project(BaseModel): + id: str + + created_at: datetime = FieldInfo(alias="createdAt") + + updated_at: datetime = FieldInfo(alias="updatedAt") diff --git a/src/browserbase/types/project_list_response.py b/src/browserbase/types/project_list_response.py new file mode 100644 index 00000000..2d05a236 --- /dev/null +++ b/src/browserbase/types/project_list_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 .project import Project + +__all__ = ["ProjectListResponse"] + +ProjectListResponse: TypeAlias = List[Project] diff --git a/src/browserbase/types/project_usage.py b/src/browserbase/types/project_usage.py new file mode 100644 index 00000000..f68cc2da --- /dev/null +++ b/src/browserbase/types/project_usage.py @@ -0,0 +1,14 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + + +from pydantic import Field as FieldInfo + +from .._models import BaseModel + +__all__ = ["ProjectUsage"] + + +class ProjectUsage(BaseModel): + browser_minutes: int = FieldInfo(alias="browserMinutes") + + proxy_bytes: int = FieldInfo(alias="proxyBytes") diff --git a/src/browserbase/types/session.py b/src/browserbase/types/session.py new file mode 100644 index 00000000..c8d7fe42 --- /dev/null +++ b/src/browserbase/types/session.py @@ -0,0 +1,17 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from datetime import datetime + +from pydantic import Field as FieldInfo + +from .._models import BaseModel + +__all__ = ["Session"] + + +class Session(BaseModel): + id: str + + created_at: datetime = FieldInfo(alias="createdAt") + + updated_at: datetime = FieldInfo(alias="updatedAt") diff --git a/src/browserbase/types/session_create_params.py b/src/browserbase/types/session_create_params.py new file mode 100644 index 00000000..fd43187f --- /dev/null +++ b/src/browserbase/types/session_create_params.py @@ -0,0 +1,186 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import List, Union, Iterable +from typing_extensions import Literal, Required, Annotated, TypeAlias, TypedDict + +from .._utils import PropertyInfo + +__all__ = [ + "SessionCreateParams", + "BrowserSettings", + "BrowserSettingsContext", + "BrowserSettingsFingerprint", + "BrowserSettingsFingerprintScreen", + "BrowserSettingsViewport", + "ProxiesUnionMember1", + "ProxiesUnionMember1BrowserbaseProxyConfig", + "ProxiesUnionMember1BrowserbaseProxyConfigGeolocation", + "ProxiesUnionMember1ExternalProxyConfig", +] + + +class SessionCreateParams(TypedDict, total=False): + project_id: Required[Annotated[str, PropertyInfo(alias="projectId")]] + """The Project ID. + + Can be found in [Settings](https://www.browserbase.com/settings). + """ + + browser_settings: Annotated[BrowserSettings, PropertyInfo(alias="browserSettings")] + + extension_id: Annotated[str, PropertyInfo(alias="extensionId")] + """The uploaded Extension ID. + + See [Upload Extension](/reference/api/upload-an-extension). + """ + + keep_alive: Annotated[bool, PropertyInfo(alias="keepAlive")] + """Set to true to keep the session alive even after disconnections. + + This is available on the Startup plan only. + """ + + proxies: Union[bool, Iterable[ProxiesUnionMember1]] + """Proxy configuration. + + Can be true for default proxy, or an array of proxy configurations. + """ + + api_timeout: Annotated[int, PropertyInfo(alias="timeout")] + """Duration in seconds after which the session will automatically end. + + Defaults to the Project's `defaultTimeout`. + """ + + +class BrowserSettingsContext(TypedDict, total=False): + id: Required[str] + """The Context ID.""" + + persist: Required[bool] + """Whether or not to persist the context after browsing. Defaults to `false`.""" + + +class BrowserSettingsFingerprintScreen(TypedDict, total=False): + max_height: Annotated[int, PropertyInfo(alias="maxHeight")] + + max_width: Annotated[int, PropertyInfo(alias="maxWidth")] + + min_height: Annotated[int, PropertyInfo(alias="minHeight")] + + min_width: Annotated[int, PropertyInfo(alias="minWidth")] + + +class BrowserSettingsFingerprint(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: List[str] + """ + Full list of locales is available + [here](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Language). + """ + + operating_systems: Annotated[ + List[Literal["android", "ios", "linux", "macos", "windows"]], PropertyInfo(alias="operatingSystems") + ] + """ + Note: `operatingSystems` set to `ios` or `android` requires `devices` to include + `"mobile"`. + """ + + screen: BrowserSettingsFingerprintScreen + + +class BrowserSettingsViewport(TypedDict, total=False): + height: int + + width: int + + +class BrowserSettings(TypedDict, total=False): + block_ads: Annotated[bool, PropertyInfo(alias="blockAds")] + """Enable or disable ad blocking in the browser. Defaults to `false`.""" + + context: BrowserSettingsContext + + extension_id: Annotated[str, PropertyInfo(alias="extensionId")] + """The uploaded Extension ID. + + See [Upload Extension](/reference/api/upload-an-extension). + """ + + fingerprint: BrowserSettingsFingerprint + """ + See usage examples + [in the Stealth Mode page](/features/stealth-mode#fingerprinting). + """ + + log_session: Annotated[bool, PropertyInfo(alias="logSession")] + """Enable or disable session logging. Defaults to `true`.""" + + record_session: Annotated[bool, PropertyInfo(alias="recordSession")] + """Enable or disable session recording. Defaults to `true`.""" + + solve_captchas: Annotated[bool, PropertyInfo(alias="solveCaptchas")] + """Enable or disable captcha solving in the browser. Defaults to `true`.""" + + viewport: BrowserSettingsViewport + + +class ProxiesUnionMember1BrowserbaseProxyConfigGeolocation(TypedDict, total=False): + country: Required[str] + """Country code in ISO 3166-1 alpha-2 format""" + + city: str + """Name of the city. Use spaces for multi-word city names. Optional.""" + + state: str + """US state code (2 characters). Must also specify US as the country. Optional.""" + + +class ProxiesUnionMember1BrowserbaseProxyConfig(TypedDict, total=False): + type: Required[Literal["browserbase"]] + """Type of proxy. + + Always use 'browserbase' for the Browserbase managed proxy network. + """ + + domain_pattern: Annotated[str, PropertyInfo(alias="domainPattern")] + """Domain pattern for which this proxy should be used. + + If omitted, defaults to all domains. Optional. + """ + + geolocation: ProxiesUnionMember1BrowserbaseProxyConfigGeolocation + """Configuration for geolocation""" + + +class ProxiesUnionMember1ExternalProxyConfig(TypedDict, total=False): + server: Required[str] + """Server URL for external proxy. Required.""" + + type: Required[Literal["external"]] + """Type of proxy. Always 'external' for this config.""" + + domain_pattern: Annotated[str, PropertyInfo(alias="domainPattern")] + """Domain pattern for which this proxy should be used. + + If omitted, defaults to all domains. Optional. + """ + + password: str + """Password for external proxy authentication. Optional.""" + + username: str + """Username for external proxy authentication. Optional.""" + + +ProxiesUnionMember1: TypeAlias = Union[ + ProxiesUnionMember1BrowserbaseProxyConfig, ProxiesUnionMember1ExternalProxyConfig +] diff --git a/src/browserbase/types/session_list_params.py b/src/browserbase/types/session_list_params.py new file mode 100644 index 00000000..7ba4798c --- /dev/null +++ b/src/browserbase/types/session_list_params.py @@ -0,0 +1,11 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Literal, TypedDict + +__all__ = ["SessionListParams"] + + +class SessionListParams(TypedDict, total=False): + status: Literal["RUNNING", "ERROR", "TIMED_OUT", "COMPLETED"] diff --git a/src/browserbase/types/session_list_response.py b/src/browserbase/types/session_list_response.py new file mode 100644 index 00000000..ca162ddb --- /dev/null +++ b/src/browserbase/types/session_list_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 .session import Session + +__all__ = ["SessionListResponse"] + +SessionListResponse: TypeAlias = List[Session] diff --git a/src/browserbase/types/session_live_urls.py b/src/browserbase/types/session_live_urls.py new file mode 100644 index 00000000..3c7ba320 --- /dev/null +++ b/src/browserbase/types/session_live_urls.py @@ -0,0 +1,33 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List + +from pydantic import Field as FieldInfo + +from .._models import BaseModel + +__all__ = ["SessionLiveURLs", "Page"] + + +class Page(BaseModel): + id: str + + debugger_fullscreen_url: str = FieldInfo(alias="debuggerFullscreenUrl") + + debugger_url: str = FieldInfo(alias="debuggerUrl") + + favicon_url: str = FieldInfo(alias="faviconUrl") + + title: str + + url: str + + +class SessionLiveURLs(BaseModel): + debugger_fullscreen_url: str = FieldInfo(alias="debuggerFullscreenUrl") + + debugger_url: str = FieldInfo(alias="debuggerUrl") + + pages: List[Page] + + ws_url: str = FieldInfo(alias="wsUrl") diff --git a/src/browserbase/types/session_update_params.py b/src/browserbase/types/session_update_params.py new file mode 100644 index 00000000..66dcd351 --- /dev/null +++ b/src/browserbase/types/session_update_params.py @@ -0,0 +1,23 @@ +# 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__ = ["SessionUpdateParams"] + + +class SessionUpdateParams(TypedDict, total=False): + project_id: Required[Annotated[str, PropertyInfo(alias="projectId")]] + """The Project ID. + + Can be found in [Settings](https://www.browserbase.com/settings). + """ + + status: Required[Literal["REQUEST_RELEASE"]] + """Set to `REQUEST_RELEASE` to request that the session complete. + + Use before session's timeout to avoid additional charges. + """ diff --git a/src/browserbase/types/sessions/__init__.py b/src/browserbase/types/sessions/__init__.py new file mode 100644 index 00000000..0cef6b19 --- /dev/null +++ b/src/browserbase/types/sessions/__init__.py @@ -0,0 +1,10 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from .session_log import SessionLog as SessionLog +from .log_list_response import LogListResponse as LogListResponse +from .session_recording import SessionRecording as SessionRecording +from .upload_create_params import UploadCreateParams as UploadCreateParams +from .upload_create_response import UploadCreateResponse as UploadCreateResponse +from .recording_retrieve_response import RecordingRetrieveResponse as RecordingRetrieveResponse diff --git a/src/browserbase/types/sessions/log_list_response.py b/src/browserbase/types/sessions/log_list_response.py new file mode 100644 index 00000000..2b325a8c --- /dev/null +++ b/src/browserbase/types/sessions/log_list_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 .session_log import SessionLog + +__all__ = ["LogListResponse"] + +LogListResponse: TypeAlias = List[SessionLog] diff --git a/src/browserbase/types/sessions/recording_retrieve_response.py b/src/browserbase/types/sessions/recording_retrieve_response.py new file mode 100644 index 00000000..951969bb --- /dev/null +++ b/src/browserbase/types/sessions/recording_retrieve_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 .session_recording import SessionRecording + +__all__ = ["RecordingRetrieveResponse"] + +RecordingRetrieveResponse: TypeAlias = List[SessionRecording] diff --git a/src/browserbase/types/sessions/session_log.py b/src/browserbase/types/sessions/session_log.py new file mode 100644 index 00000000..d15eb831 --- /dev/null +++ b/src/browserbase/types/sessions/session_log.py @@ -0,0 +1,48 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Dict, Optional + +from pydantic import Field as FieldInfo + +from ..._models import BaseModel + +__all__ = ["SessionLog", "Request", "Response"] + + +class Request(BaseModel): + params: Dict[str, object] + + raw_body: str = FieldInfo(alias="rawBody") + + timestamp: int + """milliseconds that have elapsed since the UNIX epoch""" + + +class Response(BaseModel): + raw_body: str = FieldInfo(alias="rawBody") + + result: Dict[str, object] + + timestamp: int + """milliseconds that have elapsed since the UNIX epoch""" + + +class SessionLog(BaseModel): + event_id: str = FieldInfo(alias="eventId") + + method: str + + page_id: int = FieldInfo(alias="pageId") + + session_id: str = FieldInfo(alias="sessionId") + + timestamp: int + """milliseconds that have elapsed since the UNIX epoch""" + + frame_id: Optional[str] = FieldInfo(alias="frameId", default=None) + + loader_id: Optional[str] = FieldInfo(alias="loaderId", default=None) + + request: Optional[Request] = None + + response: Optional[Response] = None diff --git a/src/browserbase/types/sessions/session_recording.py b/src/browserbase/types/sessions/session_recording.py new file mode 100644 index 00000000..d3e0325a --- /dev/null +++ b/src/browserbase/types/sessions/session_recording.py @@ -0,0 +1,26 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Dict + +from pydantic import Field as FieldInfo + +from ..._models import BaseModel + +__all__ = ["SessionRecording"] + + +class SessionRecording(BaseModel): + id: str + + data: Dict[str, object] + """ + See + [rrweb documentation](https://github.com/rrweb-io/rrweb/blob/master/docs/recipes/dive-into-event.md). + """ + + session_id: str = FieldInfo(alias="sessionId") + + timestamp: int + """milliseconds that have elapsed since the UNIX epoch""" + + type: int diff --git a/src/browserbase/types/sessions/upload_create_params.py b/src/browserbase/types/sessions/upload_create_params.py new file mode 100644 index 00000000..2bccb399 --- /dev/null +++ b/src/browserbase/types/sessions/upload_create_params.py @@ -0,0 +1,13 @@ +# 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 FileTypes + +__all__ = ["UploadCreateParams"] + + +class UploadCreateParams(TypedDict, total=False): + file: Required[FileTypes] diff --git a/src/browserbase/types/sessions/upload_create_response.py b/src/browserbase/types/sessions/upload_create_response.py new file mode 100644 index 00000000..ceece2cd --- /dev/null +++ b/src/browserbase/types/sessions/upload_create_response.py @@ -0,0 +1,10 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + + +from ..._models import BaseModel + +__all__ = ["UploadCreateResponse"] + + +class UploadCreateResponse(BaseModel): + message: str 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/sessions/__init__.py b/tests/api_resources/sessions/__init__.py new file mode 100644 index 00000000..fd8019a9 --- /dev/null +++ b/tests/api_resources/sessions/__init__.py @@ -0,0 +1 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. diff --git a/tests/api_resources/sessions/test_downloads.py b/tests/api_resources/sessions/test_downloads.py new file mode 100644 index 00000000..825ff786 --- /dev/null +++ b/tests/api_resources/sessions/test_downloads.py @@ -0,0 +1,128 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import os +from typing import Any, cast + +import httpx +import pytest +from respx import MockRouter + +from browserbase import Browserbase, AsyncBrowserbase +from browserbase._response import ( + BinaryAPIResponse, + AsyncBinaryAPIResponse, + StreamedBinaryAPIResponse, + AsyncStreamedBinaryAPIResponse, +) + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + + +class TestDownloads: + parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) + + @parametrize + @pytest.mark.respx(base_url=base_url) + def test_method_list(self, client: Browserbase, respx_mock: MockRouter) -> None: + respx_mock.get("/v1/sessions/id/downloads").mock(return_value=httpx.Response(200, json={"foo": "bar"})) + download = client.sessions.downloads.list( + "id", + ) + assert download.is_closed + assert download.json() == {"foo": "bar"} + assert cast(Any, download.is_closed) is True + assert isinstance(download, BinaryAPIResponse) + + @parametrize + @pytest.mark.respx(base_url=base_url) + def test_raw_response_list(self, client: Browserbase, respx_mock: MockRouter) -> None: + respx_mock.get("/v1/sessions/id/downloads").mock(return_value=httpx.Response(200, json={"foo": "bar"})) + + download = client.sessions.downloads.with_raw_response.list( + "id", + ) + + assert download.is_closed is True + assert download.http_request.headers.get("X-Stainless-Lang") == "python" + assert download.json() == {"foo": "bar"} + assert isinstance(download, BinaryAPIResponse) + + @parametrize + @pytest.mark.respx(base_url=base_url) + def test_streaming_response_list(self, client: Browserbase, respx_mock: MockRouter) -> None: + respx_mock.get("/v1/sessions/id/downloads").mock(return_value=httpx.Response(200, json={"foo": "bar"})) + with client.sessions.downloads.with_streaming_response.list( + "id", + ) as download: + assert not download.is_closed + assert download.http_request.headers.get("X-Stainless-Lang") == "python" + + assert download.json() == {"foo": "bar"} + assert cast(Any, download.is_closed) is True + assert isinstance(download, StreamedBinaryAPIResponse) + + assert cast(Any, download.is_closed) is True + + @parametrize + @pytest.mark.respx(base_url=base_url) + def test_path_params_list(self, client: Browserbase) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.sessions.downloads.with_raw_response.list( + "", + ) + + +class TestAsyncDownloads: + parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"]) + + @parametrize + @pytest.mark.respx(base_url=base_url) + async def test_method_list(self, async_client: AsyncBrowserbase, respx_mock: MockRouter) -> None: + respx_mock.get("/v1/sessions/id/downloads").mock(return_value=httpx.Response(200, json={"foo": "bar"})) + download = await async_client.sessions.downloads.list( + "id", + ) + assert download.is_closed + assert await download.json() == {"foo": "bar"} + assert cast(Any, download.is_closed) is True + assert isinstance(download, AsyncBinaryAPIResponse) + + @parametrize + @pytest.mark.respx(base_url=base_url) + async def test_raw_response_list(self, async_client: AsyncBrowserbase, respx_mock: MockRouter) -> None: + respx_mock.get("/v1/sessions/id/downloads").mock(return_value=httpx.Response(200, json={"foo": "bar"})) + + download = await async_client.sessions.downloads.with_raw_response.list( + "id", + ) + + assert download.is_closed is True + assert download.http_request.headers.get("X-Stainless-Lang") == "python" + assert await download.json() == {"foo": "bar"} + assert isinstance(download, AsyncBinaryAPIResponse) + + @parametrize + @pytest.mark.respx(base_url=base_url) + async def test_streaming_response_list(self, async_client: AsyncBrowserbase, respx_mock: MockRouter) -> None: + respx_mock.get("/v1/sessions/id/downloads").mock(return_value=httpx.Response(200, json={"foo": "bar"})) + async with async_client.sessions.downloads.with_streaming_response.list( + "id", + ) as download: + assert not download.is_closed + assert download.http_request.headers.get("X-Stainless-Lang") == "python" + + assert await download.json() == {"foo": "bar"} + assert cast(Any, download.is_closed) is True + assert isinstance(download, AsyncStreamedBinaryAPIResponse) + + assert cast(Any, download.is_closed) is True + + @parametrize + @pytest.mark.respx(base_url=base_url) + async def test_path_params_list(self, async_client: AsyncBrowserbase) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.sessions.downloads.with_raw_response.list( + "", + ) diff --git a/tests/api_resources/sessions/test_logs.py b/tests/api_resources/sessions/test_logs.py new file mode 100644 index 00000000..c72002b3 --- /dev/null +++ b/tests/api_resources/sessions/test_logs.py @@ -0,0 +1,98 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import os +from typing import Any, cast + +import pytest + +from browserbase import Browserbase, AsyncBrowserbase +from tests.utils import assert_matches_type +from browserbase.types.sessions import LogListResponse + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + + +class TestLogs: + parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) + + @parametrize + def test_method_list(self, client: Browserbase) -> None: + log = client.sessions.logs.list( + "id", + ) + assert_matches_type(LogListResponse, log, path=["response"]) + + @parametrize + def test_raw_response_list(self, client: Browserbase) -> None: + response = client.sessions.logs.with_raw_response.list( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + log = response.parse() + assert_matches_type(LogListResponse, log, path=["response"]) + + @parametrize + def test_streaming_response_list(self, client: Browserbase) -> None: + with client.sessions.logs.with_streaming_response.list( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + log = response.parse() + assert_matches_type(LogListResponse, log, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_list(self, client: Browserbase) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.sessions.logs.with_raw_response.list( + "", + ) + + +class TestAsyncLogs: + parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"]) + + @parametrize + async def test_method_list(self, async_client: AsyncBrowserbase) -> None: + log = await async_client.sessions.logs.list( + "id", + ) + assert_matches_type(LogListResponse, log, path=["response"]) + + @parametrize + async def test_raw_response_list(self, async_client: AsyncBrowserbase) -> None: + response = await async_client.sessions.logs.with_raw_response.list( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + log = await response.parse() + assert_matches_type(LogListResponse, log, path=["response"]) + + @parametrize + async def test_streaming_response_list(self, async_client: AsyncBrowserbase) -> None: + async with async_client.sessions.logs.with_streaming_response.list( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + log = await response.parse() + assert_matches_type(LogListResponse, log, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_list(self, async_client: AsyncBrowserbase) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.sessions.logs.with_raw_response.list( + "", + ) diff --git a/tests/api_resources/sessions/test_recording.py b/tests/api_resources/sessions/test_recording.py new file mode 100644 index 00000000..0d7a542e --- /dev/null +++ b/tests/api_resources/sessions/test_recording.py @@ -0,0 +1,98 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import os +from typing import Any, cast + +import pytest + +from browserbase import Browserbase, AsyncBrowserbase +from tests.utils import assert_matches_type +from browserbase.types.sessions import RecordingRetrieveResponse + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + + +class TestRecording: + parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) + + @parametrize + def test_method_retrieve(self, client: Browserbase) -> None: + recording = client.sessions.recording.retrieve( + "id", + ) + assert_matches_type(RecordingRetrieveResponse, recording, path=["response"]) + + @parametrize + def test_raw_response_retrieve(self, client: Browserbase) -> None: + response = client.sessions.recording.with_raw_response.retrieve( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + recording = response.parse() + assert_matches_type(RecordingRetrieveResponse, recording, path=["response"]) + + @parametrize + def test_streaming_response_retrieve(self, client: Browserbase) -> None: + with client.sessions.recording.with_streaming_response.retrieve( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + recording = response.parse() + assert_matches_type(RecordingRetrieveResponse, recording, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_retrieve(self, client: Browserbase) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.sessions.recording.with_raw_response.retrieve( + "", + ) + + +class TestAsyncRecording: + parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"]) + + @parametrize + async def test_method_retrieve(self, async_client: AsyncBrowserbase) -> None: + recording = await async_client.sessions.recording.retrieve( + "id", + ) + assert_matches_type(RecordingRetrieveResponse, recording, path=["response"]) + + @parametrize + async def test_raw_response_retrieve(self, async_client: AsyncBrowserbase) -> None: + response = await async_client.sessions.recording.with_raw_response.retrieve( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + recording = await response.parse() + assert_matches_type(RecordingRetrieveResponse, recording, path=["response"]) + + @parametrize + async def test_streaming_response_retrieve(self, async_client: AsyncBrowserbase) -> None: + async with async_client.sessions.recording.with_streaming_response.retrieve( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + recording = await response.parse() + assert_matches_type(RecordingRetrieveResponse, recording, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_retrieve(self, async_client: AsyncBrowserbase) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.sessions.recording.with_raw_response.retrieve( + "", + ) diff --git a/tests/api_resources/sessions/test_uploads.py b/tests/api_resources/sessions/test_uploads.py new file mode 100644 index 00000000..f193256c --- /dev/null +++ b/tests/api_resources/sessions/test_uploads.py @@ -0,0 +1,106 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import os +from typing import Any, cast + +import pytest + +from browserbase import Browserbase, AsyncBrowserbase +from tests.utils import assert_matches_type +from browserbase.types.sessions import UploadCreateResponse + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + + +class TestUploads: + parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) + + @parametrize + def test_method_create(self, client: Browserbase) -> None: + upload = client.sessions.uploads.create( + id="id", + file=b"raw file contents", + ) + assert_matches_type(UploadCreateResponse, upload, path=["response"]) + + @parametrize + def test_raw_response_create(self, client: Browserbase) -> None: + response = client.sessions.uploads.with_raw_response.create( + id="id", + file=b"raw file contents", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + upload = response.parse() + assert_matches_type(UploadCreateResponse, upload, path=["response"]) + + @parametrize + def test_streaming_response_create(self, client: Browserbase) -> None: + with client.sessions.uploads.with_streaming_response.create( + id="id", + file=b"raw file contents", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + upload = response.parse() + assert_matches_type(UploadCreateResponse, upload, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_create(self, client: Browserbase) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.sessions.uploads.with_raw_response.create( + id="", + file=b"raw file contents", + ) + + +class TestAsyncUploads: + parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"]) + + @parametrize + async def test_method_create(self, async_client: AsyncBrowserbase) -> None: + upload = await async_client.sessions.uploads.create( + id="id", + file=b"raw file contents", + ) + assert_matches_type(UploadCreateResponse, upload, path=["response"]) + + @parametrize + async def test_raw_response_create(self, async_client: AsyncBrowserbase) -> None: + response = await async_client.sessions.uploads.with_raw_response.create( + id="id", + file=b"raw file contents", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + upload = await response.parse() + assert_matches_type(UploadCreateResponse, upload, path=["response"]) + + @parametrize + async def test_streaming_response_create(self, async_client: AsyncBrowserbase) -> None: + async with async_client.sessions.uploads.with_streaming_response.create( + id="id", + file=b"raw file contents", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + upload = await response.parse() + assert_matches_type(UploadCreateResponse, upload, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_create(self, async_client: AsyncBrowserbase) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.sessions.uploads.with_raw_response.create( + id="", + file=b"raw file contents", + ) diff --git a/tests/api_resources/test_contexts.py b/tests/api_resources/test_contexts.py new file mode 100644 index 00000000..e53b7e11 --- /dev/null +++ b/tests/api_resources/test_contexts.py @@ -0,0 +1,236 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import os +from typing import Any, cast + +import pytest + +from browserbase import Browserbase, AsyncBrowserbase +from tests.utils import assert_matches_type +from browserbase.types import Context, ContextCreateResponse, ContextUpdateResponse + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + + +class TestContexts: + parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) + + @parametrize + def test_method_create(self, client: Browserbase) -> None: + context = client.contexts.create( + project_id="projectId", + ) + assert_matches_type(ContextCreateResponse, context, path=["response"]) + + @parametrize + def test_raw_response_create(self, client: Browserbase) -> None: + response = client.contexts.with_raw_response.create( + project_id="projectId", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + context = response.parse() + assert_matches_type(ContextCreateResponse, context, path=["response"]) + + @parametrize + def test_streaming_response_create(self, client: Browserbase) -> None: + with client.contexts.with_streaming_response.create( + project_id="projectId", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + context = response.parse() + assert_matches_type(ContextCreateResponse, context, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_method_retrieve(self, client: Browserbase) -> None: + context = client.contexts.retrieve( + "id", + ) + assert_matches_type(Context, context, path=["response"]) + + @parametrize + def test_raw_response_retrieve(self, client: Browserbase) -> None: + response = client.contexts.with_raw_response.retrieve( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + context = response.parse() + assert_matches_type(Context, context, path=["response"]) + + @parametrize + def test_streaming_response_retrieve(self, client: Browserbase) -> None: + with client.contexts.with_streaming_response.retrieve( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + context = response.parse() + assert_matches_type(Context, context, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_retrieve(self, client: Browserbase) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.contexts.with_raw_response.retrieve( + "", + ) + + @parametrize + def test_method_update(self, client: Browserbase) -> None: + context = client.contexts.update( + "id", + ) + assert_matches_type(ContextUpdateResponse, context, path=["response"]) + + @parametrize + def test_raw_response_update(self, client: Browserbase) -> None: + response = client.contexts.with_raw_response.update( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + context = response.parse() + assert_matches_type(ContextUpdateResponse, context, path=["response"]) + + @parametrize + def test_streaming_response_update(self, client: Browserbase) -> None: + with client.contexts.with_streaming_response.update( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + context = response.parse() + assert_matches_type(ContextUpdateResponse, context, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_update(self, client: Browserbase) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.contexts.with_raw_response.update( + "", + ) + + +class TestAsyncContexts: + parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"]) + + @parametrize + async def test_method_create(self, async_client: AsyncBrowserbase) -> None: + context = await async_client.contexts.create( + project_id="projectId", + ) + assert_matches_type(ContextCreateResponse, context, path=["response"]) + + @parametrize + async def test_raw_response_create(self, async_client: AsyncBrowserbase) -> None: + response = await async_client.contexts.with_raw_response.create( + project_id="projectId", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + context = await response.parse() + assert_matches_type(ContextCreateResponse, context, path=["response"]) + + @parametrize + async def test_streaming_response_create(self, async_client: AsyncBrowserbase) -> None: + async with async_client.contexts.with_streaming_response.create( + project_id="projectId", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + context = await response.parse() + assert_matches_type(ContextCreateResponse, context, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_method_retrieve(self, async_client: AsyncBrowserbase) -> None: + context = await async_client.contexts.retrieve( + "id", + ) + assert_matches_type(Context, context, path=["response"]) + + @parametrize + async def test_raw_response_retrieve(self, async_client: AsyncBrowserbase) -> None: + response = await async_client.contexts.with_raw_response.retrieve( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + context = await response.parse() + assert_matches_type(Context, context, path=["response"]) + + @parametrize + async def test_streaming_response_retrieve(self, async_client: AsyncBrowserbase) -> None: + async with async_client.contexts.with_streaming_response.retrieve( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + context = await response.parse() + assert_matches_type(Context, context, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_retrieve(self, async_client: AsyncBrowserbase) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.contexts.with_raw_response.retrieve( + "", + ) + + @parametrize + async def test_method_update(self, async_client: AsyncBrowserbase) -> None: + context = await async_client.contexts.update( + "id", + ) + assert_matches_type(ContextUpdateResponse, context, path=["response"]) + + @parametrize + async def test_raw_response_update(self, async_client: AsyncBrowserbase) -> None: + response = await async_client.contexts.with_raw_response.update( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + context = await response.parse() + assert_matches_type(ContextUpdateResponse, context, path=["response"]) + + @parametrize + async def test_streaming_response_update(self, async_client: AsyncBrowserbase) -> None: + async with async_client.contexts.with_streaming_response.update( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + context = await response.parse() + assert_matches_type(ContextUpdateResponse, context, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_update(self, async_client: AsyncBrowserbase) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.contexts.with_raw_response.update( + "", + ) diff --git a/tests/api_resources/test_extensions.py b/tests/api_resources/test_extensions.py new file mode 100644 index 00000000..b7fec7a5 --- /dev/null +++ b/tests/api_resources/test_extensions.py @@ -0,0 +1,236 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import os +from typing import Any, cast + +import pytest + +from browserbase import Browserbase, AsyncBrowserbase +from tests.utils import assert_matches_type +from browserbase.types import Extension + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + + +class TestExtensions: + parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) + + @parametrize + def test_method_create(self, client: Browserbase) -> None: + extension = client.extensions.create( + file=b"raw file contents", + ) + assert_matches_type(Extension, extension, path=["response"]) + + @parametrize + def test_raw_response_create(self, client: Browserbase) -> None: + response = client.extensions.with_raw_response.create( + file=b"raw file contents", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + extension = response.parse() + assert_matches_type(Extension, extension, path=["response"]) + + @parametrize + def test_streaming_response_create(self, client: Browserbase) -> None: + with client.extensions.with_streaming_response.create( + file=b"raw file contents", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + extension = response.parse() + assert_matches_type(Extension, extension, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_method_retrieve(self, client: Browserbase) -> None: + extension = client.extensions.retrieve( + "id", + ) + assert_matches_type(Extension, extension, path=["response"]) + + @parametrize + def test_raw_response_retrieve(self, client: Browserbase) -> None: + response = client.extensions.with_raw_response.retrieve( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + extension = response.parse() + assert_matches_type(Extension, extension, path=["response"]) + + @parametrize + def test_streaming_response_retrieve(self, client: Browserbase) -> None: + with client.extensions.with_streaming_response.retrieve( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + extension = response.parse() + assert_matches_type(Extension, extension, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_retrieve(self, client: Browserbase) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.extensions.with_raw_response.retrieve( + "", + ) + + @parametrize + def test_method_delete(self, client: Browserbase) -> None: + extension = client.extensions.delete( + "id", + ) + assert extension is None + + @parametrize + def test_raw_response_delete(self, client: Browserbase) -> None: + response = client.extensions.with_raw_response.delete( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + extension = response.parse() + assert extension is None + + @parametrize + def test_streaming_response_delete(self, client: Browserbase) -> None: + with client.extensions.with_streaming_response.delete( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + extension = response.parse() + assert extension is None + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_delete(self, client: Browserbase) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.extensions.with_raw_response.delete( + "", + ) + + +class TestAsyncExtensions: + parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"]) + + @parametrize + async def test_method_create(self, async_client: AsyncBrowserbase) -> None: + extension = await async_client.extensions.create( + file=b"raw file contents", + ) + assert_matches_type(Extension, extension, path=["response"]) + + @parametrize + async def test_raw_response_create(self, async_client: AsyncBrowserbase) -> None: + response = await async_client.extensions.with_raw_response.create( + file=b"raw file contents", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + extension = await response.parse() + assert_matches_type(Extension, extension, path=["response"]) + + @parametrize + async def test_streaming_response_create(self, async_client: AsyncBrowserbase) -> None: + async with async_client.extensions.with_streaming_response.create( + file=b"raw file contents", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + extension = await response.parse() + assert_matches_type(Extension, extension, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_method_retrieve(self, async_client: AsyncBrowserbase) -> None: + extension = await async_client.extensions.retrieve( + "id", + ) + assert_matches_type(Extension, extension, path=["response"]) + + @parametrize + async def test_raw_response_retrieve(self, async_client: AsyncBrowserbase) -> None: + response = await async_client.extensions.with_raw_response.retrieve( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + extension = await response.parse() + assert_matches_type(Extension, extension, path=["response"]) + + @parametrize + async def test_streaming_response_retrieve(self, async_client: AsyncBrowserbase) -> None: + async with async_client.extensions.with_streaming_response.retrieve( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + extension = await response.parse() + assert_matches_type(Extension, extension, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_retrieve(self, async_client: AsyncBrowserbase) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.extensions.with_raw_response.retrieve( + "", + ) + + @parametrize + async def test_method_delete(self, async_client: AsyncBrowserbase) -> None: + extension = await async_client.extensions.delete( + "id", + ) + assert extension is None + + @parametrize + async def test_raw_response_delete(self, async_client: AsyncBrowserbase) -> None: + response = await async_client.extensions.with_raw_response.delete( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + extension = await response.parse() + assert extension is None + + @parametrize + async def test_streaming_response_delete(self, async_client: AsyncBrowserbase) -> None: + async with async_client.extensions.with_streaming_response.delete( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + extension = await response.parse() + assert extension is None + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_delete(self, async_client: AsyncBrowserbase) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.extensions.with_raw_response.delete( + "", + ) diff --git a/tests/api_resources/test_projects.py b/tests/api_resources/test_projects.py new file mode 100644 index 00000000..9e70d034 --- /dev/null +++ b/tests/api_resources/test_projects.py @@ -0,0 +1,224 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import os +from typing import Any, cast + +import pytest + +from browserbase import Browserbase, AsyncBrowserbase +from tests.utils import assert_matches_type +from browserbase.types import Project, ProjectUsage, ProjectListResponse + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + + +class TestProjects: + parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) + + @parametrize + def test_method_retrieve(self, client: Browserbase) -> None: + project = client.projects.retrieve( + "id", + ) + assert_matches_type(Project, project, path=["response"]) + + @parametrize + def test_raw_response_retrieve(self, client: Browserbase) -> None: + response = client.projects.with_raw_response.retrieve( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + project = response.parse() + assert_matches_type(Project, project, path=["response"]) + + @parametrize + def test_streaming_response_retrieve(self, client: Browserbase) -> None: + with client.projects.with_streaming_response.retrieve( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + project = response.parse() + assert_matches_type(Project, project, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_retrieve(self, client: Browserbase) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.projects.with_raw_response.retrieve( + "", + ) + + @parametrize + def test_method_list(self, client: Browserbase) -> None: + project = client.projects.list() + assert_matches_type(ProjectListResponse, project, path=["response"]) + + @parametrize + def test_raw_response_list(self, client: Browserbase) -> None: + response = client.projects.with_raw_response.list() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + project = response.parse() + assert_matches_type(ProjectListResponse, project, path=["response"]) + + @parametrize + def test_streaming_response_list(self, client: Browserbase) -> None: + with client.projects.with_streaming_response.list() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + project = response.parse() + assert_matches_type(ProjectListResponse, project, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_method_usage(self, client: Browserbase) -> None: + project = client.projects.usage( + "id", + ) + assert_matches_type(ProjectUsage, project, path=["response"]) + + @parametrize + def test_raw_response_usage(self, client: Browserbase) -> None: + response = client.projects.with_raw_response.usage( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + project = response.parse() + assert_matches_type(ProjectUsage, project, path=["response"]) + + @parametrize + def test_streaming_response_usage(self, client: Browserbase) -> None: + with client.projects.with_streaming_response.usage( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + project = response.parse() + assert_matches_type(ProjectUsage, project, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_usage(self, client: Browserbase) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.projects.with_raw_response.usage( + "", + ) + + +class TestAsyncProjects: + parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"]) + + @parametrize + async def test_method_retrieve(self, async_client: AsyncBrowserbase) -> None: + project = await async_client.projects.retrieve( + "id", + ) + assert_matches_type(Project, project, path=["response"]) + + @parametrize + async def test_raw_response_retrieve(self, async_client: AsyncBrowserbase) -> None: + response = await async_client.projects.with_raw_response.retrieve( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + project = await response.parse() + assert_matches_type(Project, project, path=["response"]) + + @parametrize + async def test_streaming_response_retrieve(self, async_client: AsyncBrowserbase) -> None: + async with async_client.projects.with_streaming_response.retrieve( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + project = await response.parse() + assert_matches_type(Project, project, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_retrieve(self, async_client: AsyncBrowserbase) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.projects.with_raw_response.retrieve( + "", + ) + + @parametrize + async def test_method_list(self, async_client: AsyncBrowserbase) -> None: + project = await async_client.projects.list() + assert_matches_type(ProjectListResponse, project, path=["response"]) + + @parametrize + async def test_raw_response_list(self, async_client: AsyncBrowserbase) -> None: + response = await async_client.projects.with_raw_response.list() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + project = await response.parse() + assert_matches_type(ProjectListResponse, project, path=["response"]) + + @parametrize + async def test_streaming_response_list(self, async_client: AsyncBrowserbase) -> None: + async with async_client.projects.with_streaming_response.list() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + project = await response.parse() + assert_matches_type(ProjectListResponse, project, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_method_usage(self, async_client: AsyncBrowserbase) -> None: + project = await async_client.projects.usage( + "id", + ) + assert_matches_type(ProjectUsage, project, path=["response"]) + + @parametrize + async def test_raw_response_usage(self, async_client: AsyncBrowserbase) -> None: + response = await async_client.projects.with_raw_response.usage( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + project = await response.parse() + assert_matches_type(ProjectUsage, project, path=["response"]) + + @parametrize + async def test_streaming_response_usage(self, async_client: AsyncBrowserbase) -> None: + async with async_client.projects.with_streaming_response.usage( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + project = await response.parse() + assert_matches_type(ProjectUsage, project, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_usage(self, async_client: AsyncBrowserbase) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.projects.with_raw_response.usage( + "", + ) diff --git a/tests/api_resources/test_sessions.py b/tests/api_resources/test_sessions.py new file mode 100644 index 00000000..40b71737 --- /dev/null +++ b/tests/api_resources/test_sessions.py @@ -0,0 +1,474 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import os +from typing import Any, cast + +import pytest + +from browserbase import Browserbase, AsyncBrowserbase +from tests.utils import assert_matches_type +from browserbase.types import ( + Session, + SessionLiveURLs, + SessionListResponse, +) + +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"]) + + @parametrize + def test_method_create(self, client: Browserbase) -> None: + session = client.sessions.create( + project_id="projectId", + ) + assert_matches_type(Session, session, path=["response"]) + + @parametrize + def test_method_create_with_all_params(self, client: Browserbase) -> None: + session = client.sessions.create( + project_id="projectId", + browser_settings={ + "block_ads": True, + "context": { + "id": "id", + "persist": True, + }, + "extension_id": "extensionId", + "fingerprint": { + "browsers": ["chrome", "edge", "firefox"], + "devices": ["desktop", "mobile"], + "http_version": 1, + "locales": ["string", "string", "string"], + "operating_systems": ["android", "ios", "linux"], + "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, + proxies=True, + api_timeout=60, + ) + assert_matches_type(Session, session, path=["response"]) + + @parametrize + def test_raw_response_create(self, client: Browserbase) -> None: + response = client.sessions.with_raw_response.create( + project_id="projectId", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + session = response.parse() + assert_matches_type(Session, session, path=["response"]) + + @parametrize + def test_streaming_response_create(self, client: Browserbase) -> None: + with client.sessions.with_streaming_response.create( + project_id="projectId", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + session = response.parse() + assert_matches_type(Session, session, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_method_retrieve(self, client: Browserbase) -> None: + session = client.sessions.retrieve( + "id", + ) + assert_matches_type(Session, session, path=["response"]) + + @parametrize + def test_raw_response_retrieve(self, client: Browserbase) -> None: + response = client.sessions.with_raw_response.retrieve( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + session = response.parse() + assert_matches_type(Session, session, path=["response"]) + + @parametrize + def test_streaming_response_retrieve(self, client: Browserbase) -> None: + with client.sessions.with_streaming_response.retrieve( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + session = response.parse() + assert_matches_type(Session, session, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_retrieve(self, client: Browserbase) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.sessions.with_raw_response.retrieve( + "", + ) + + @parametrize + def test_method_update(self, client: Browserbase) -> None: + session = client.sessions.update( + id="id", + project_id="projectId", + status="REQUEST_RELEASE", + ) + assert_matches_type(Session, session, path=["response"]) + + @parametrize + def test_raw_response_update(self, client: Browserbase) -> None: + response = client.sessions.with_raw_response.update( + id="id", + project_id="projectId", + status="REQUEST_RELEASE", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + session = response.parse() + assert_matches_type(Session, session, path=["response"]) + + @parametrize + def test_streaming_response_update(self, client: Browserbase) -> None: + with client.sessions.with_streaming_response.update( + id="id", + project_id="projectId", + status="REQUEST_RELEASE", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + session = response.parse() + assert_matches_type(Session, session, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_update(self, client: Browserbase) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.sessions.with_raw_response.update( + id="", + project_id="projectId", + status="REQUEST_RELEASE", + ) + + @parametrize + def test_method_list(self, client: Browserbase) -> None: + session = client.sessions.list() + assert_matches_type(SessionListResponse, session, path=["response"]) + + @parametrize + def test_method_list_with_all_params(self, client: Browserbase) -> None: + session = client.sessions.list( + status="RUNNING", + ) + assert_matches_type(SessionListResponse, session, path=["response"]) + + @parametrize + def test_raw_response_list(self, client: Browserbase) -> None: + response = client.sessions.with_raw_response.list() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + session = response.parse() + assert_matches_type(SessionListResponse, session, path=["response"]) + + @parametrize + def test_streaming_response_list(self, client: Browserbase) -> None: + with client.sessions.with_streaming_response.list() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + session = response.parse() + assert_matches_type(SessionListResponse, session, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_method_debug(self, client: Browserbase) -> None: + session = client.sessions.debug( + "id", + ) + assert_matches_type(SessionLiveURLs, session, path=["response"]) + + @parametrize + def test_raw_response_debug(self, client: Browserbase) -> None: + response = client.sessions.with_raw_response.debug( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + session = response.parse() + assert_matches_type(SessionLiveURLs, session, path=["response"]) + + @parametrize + def test_streaming_response_debug(self, client: Browserbase) -> None: + with client.sessions.with_streaming_response.debug( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + session = response.parse() + assert_matches_type(SessionLiveURLs, session, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_debug(self, client: Browserbase) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.sessions.with_raw_response.debug( + "", + ) + + +class TestAsyncSessions: + parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"]) + + @parametrize + async def test_method_create(self, async_client: AsyncBrowserbase) -> None: + session = await async_client.sessions.create( + project_id="projectId", + ) + assert_matches_type(Session, session, path=["response"]) + + @parametrize + async def test_method_create_with_all_params(self, async_client: AsyncBrowserbase) -> None: + session = await async_client.sessions.create( + project_id="projectId", + browser_settings={ + "block_ads": True, + "context": { + "id": "id", + "persist": True, + }, + "extension_id": "extensionId", + "fingerprint": { + "browsers": ["chrome", "edge", "firefox"], + "devices": ["desktop", "mobile"], + "http_version": 1, + "locales": ["string", "string", "string"], + "operating_systems": ["android", "ios", "linux"], + "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, + proxies=True, + api_timeout=60, + ) + assert_matches_type(Session, session, path=["response"]) + + @parametrize + async def test_raw_response_create(self, async_client: AsyncBrowserbase) -> None: + response = await async_client.sessions.with_raw_response.create( + project_id="projectId", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + session = await response.parse() + assert_matches_type(Session, session, path=["response"]) + + @parametrize + async def test_streaming_response_create(self, async_client: AsyncBrowserbase) -> None: + async with async_client.sessions.with_streaming_response.create( + project_id="projectId", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + session = await response.parse() + assert_matches_type(Session, session, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_method_retrieve(self, async_client: AsyncBrowserbase) -> None: + session = await async_client.sessions.retrieve( + "id", + ) + assert_matches_type(Session, session, path=["response"]) + + @parametrize + async def test_raw_response_retrieve(self, async_client: AsyncBrowserbase) -> None: + response = await async_client.sessions.with_raw_response.retrieve( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + session = await response.parse() + assert_matches_type(Session, session, path=["response"]) + + @parametrize + async def test_streaming_response_retrieve(self, async_client: AsyncBrowserbase) -> None: + async with async_client.sessions.with_streaming_response.retrieve( + "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(Session, session, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_retrieve(self, async_client: AsyncBrowserbase) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.sessions.with_raw_response.retrieve( + "", + ) + + @parametrize + async def test_method_update(self, async_client: AsyncBrowserbase) -> None: + session = await async_client.sessions.update( + id="id", + project_id="projectId", + status="REQUEST_RELEASE", + ) + assert_matches_type(Session, session, path=["response"]) + + @parametrize + async def test_raw_response_update(self, async_client: AsyncBrowserbase) -> None: + response = await async_client.sessions.with_raw_response.update( + id="id", + project_id="projectId", + status="REQUEST_RELEASE", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + session = await response.parse() + assert_matches_type(Session, session, path=["response"]) + + @parametrize + async def test_streaming_response_update(self, async_client: AsyncBrowserbase) -> None: + async with async_client.sessions.with_streaming_response.update( + id="id", + project_id="projectId", + status="REQUEST_RELEASE", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + session = await response.parse() + assert_matches_type(Session, session, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_update(self, async_client: AsyncBrowserbase) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.sessions.with_raw_response.update( + id="", + project_id="projectId", + status="REQUEST_RELEASE", + ) + + @parametrize + async def test_method_list(self, async_client: AsyncBrowserbase) -> None: + session = await async_client.sessions.list() + assert_matches_type(SessionListResponse, session, path=["response"]) + + @parametrize + async def test_method_list_with_all_params(self, async_client: AsyncBrowserbase) -> None: + session = await async_client.sessions.list( + status="RUNNING", + ) + assert_matches_type(SessionListResponse, session, path=["response"]) + + @parametrize + async def test_raw_response_list(self, async_client: AsyncBrowserbase) -> None: + response = await async_client.sessions.with_raw_response.list() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + session = await response.parse() + assert_matches_type(SessionListResponse, session, path=["response"]) + + @parametrize + async def test_streaming_response_list(self, async_client: AsyncBrowserbase) -> None: + async with async_client.sessions.with_streaming_response.list() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + session = await response.parse() + assert_matches_type(SessionListResponse, session, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_method_debug(self, async_client: AsyncBrowserbase) -> None: + session = await async_client.sessions.debug( + "id", + ) + assert_matches_type(SessionLiveURLs, session, path=["response"]) + + @parametrize + async def test_raw_response_debug(self, async_client: AsyncBrowserbase) -> None: + response = await async_client.sessions.with_raw_response.debug( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + session = await response.parse() + assert_matches_type(SessionLiveURLs, session, path=["response"]) + + @parametrize + async def test_streaming_response_debug(self, async_client: AsyncBrowserbase) -> None: + async with async_client.sessions.with_streaming_response.debug( + "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(SessionLiveURLs, session, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_debug(self, async_client: AsyncBrowserbase) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.sessions.with_raw_response.debug( + "", + ) diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 00000000..06a9e893 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,49 @@ +from __future__ import annotations + +import os +import asyncio +import logging +from typing import TYPE_CHECKING, Iterator, AsyncIterator + +import pytest + +from browserbase import Browserbase, AsyncBrowserbase + +if TYPE_CHECKING: + from _pytest.fixtures import FixtureRequest + +pytest.register_assert_rewrite("tests.utils") + +logging.getLogger("browserbase").setLevel(logging.DEBUG) + + +@pytest.fixture(scope="session") +def event_loop() -> Iterator[asyncio.AbstractEventLoop]: + loop = asyncio.new_event_loop() + yield loop + loop.close() + + +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[Browserbase]: + 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: + yield client + + +@pytest.fixture(scope="session") +async def async_client(request: FixtureRequest) -> AsyncIterator[AsyncBrowserbase]: + strict = getattr(request, "param", True) + if not isinstance(strict, bool): + raise TypeError(f"Unexpected fixture parameter type {type(strict)}, expected {bool}") + + async with AsyncBrowserbase(base_url=base_url, api_key=api_key, _strict_response_validation=strict) 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..fe8d7f86 --- /dev/null +++ b/tests/test_client.py @@ -0,0 +1,1610 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import gc +import os +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 browserbase import Browserbase, AsyncBrowserbase, APIResponseValidationError +from browserbase._types import Omit +from browserbase._models import BaseModel, FinalRequestOptions +from browserbase._constants import RAW_RESPONSE_HEADER +from browserbase._exceptions import APIStatusError, APITimeoutError, BrowserbaseError, APIResponseValidationError +from browserbase._base_client import ( + DEFAULT_TIMEOUT, + HTTPX_DEFAULT_TIMEOUT, + BaseClient, + 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: Browserbase | AsyncBrowserbase) -> int: + transport = client._client._transport + assert isinstance(transport, httpx.HTTPTransport) or isinstance(transport, httpx.AsyncHTTPTransport) + + pool = transport._pool + return len(pool._requests) + + +class TestBrowserbase: + client = Browserbase(base_url=base_url, api_key=api_key, _strict_response_validation=True) + + @pytest.mark.respx(base_url=base_url) + def test_raw_response(self, respx_mock: MockRouter) -> None: + respx_mock.post("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"})) + + response = self.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) -> None: + respx_mock.post("/foo").mock( + return_value=httpx.Response(200, headers={"Content-Type": "application/binary"}, content='{"foo": "bar"}') + ) + + response = self.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) -> None: + copied = self.client.copy() + assert id(copied) != id(self.client) + + copied = self.client.copy(api_key="another My API Key") + assert copied.api_key == "another My API Key" + assert self.client.api_key == "My API Key" + + def test_copy_default_options(self) -> None: + # options that have a default are overridden correctly + copied = self.client.copy(max_retries=7) + assert copied.max_retries == 7 + assert self.client.max_retries == 2 + + copied2 = copied.copy(max_retries=6) + assert copied2.max_retries == 6 + assert copied.max_retries == 7 + + # timeout + assert isinstance(self.client.timeout, httpx.Timeout) + copied = self.client.copy(timeout=None) + assert copied.timeout is None + assert isinstance(self.client.timeout, httpx.Timeout) + + def test_copy_default_headers(self) -> None: + 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" + + # 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"}) + + def test_copy_default_query(self) -> None: + client = Browserbase( + 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"}) + + def test_copy_signature(self) -> 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. + self.client.__init__, # type: ignore[misc] + ) + copy_signature = inspect.signature(self.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" + + def test_copy_build_request(self) -> None: + options = FinalRequestOptions(method="get", url="/foo") + + def build_request(options: FinalRequestOptions) -> None: + client = self.client.copy() + client._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. + "browserbase/_legacy_response.py", + "browserbase/_response.py", + # pydantic.BaseModel.model_dump || pydantic.BaseModel.dict leak memory for some reason. + "browserbase/_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) -> None: + request = self.client._build_request(FinalRequestOptions(method="get", url="/foo")) + timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore + assert timeout == DEFAULT_TIMEOUT + + request = self.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 = Browserbase( + 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) + + 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( + 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) + + # no timeout given to the httpx client should not use the httpx default + with httpx.Client() as http_client: + client = Browserbase( + 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 + + # explicitly passing the default timeout currently results in it being ignored + with httpx.Client(timeout=HTTPX_DEFAULT_TIMEOUT) as http_client: + client = Browserbase( + 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 + + 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( + 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: + client = Browserbase( + base_url=base_url, api_key=api_key, _strict_response_validation=True, default_headers={"X-Foo": "bar"} + ) + request = client._build_request(FinalRequestOptions(method="get", url="/foo")) + assert request.headers.get("x-foo") == "bar" + assert request.headers.get("x-stainless-lang") == "python" + + client2 = Browserbase( + base_url=base_url, + api_key=api_key, + _strict_response_validation=True, + default_headers={ + "X-Foo": "stainless", + "X-Stainless-Lang": "my-overriding-header", + }, + ) + request = 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" + + def test_validate_headers(self) -> None: + 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("X-BB-API-Key") == api_key + + with pytest.raises(BrowserbaseError): + with update_env(**{"BROWSERBASE_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 = 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")) + 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": "overriden"}, + ) + ) + url = httpx.URL(request.url) + assert dict(url.params) == {"foo": "baz", "query_param": "overriden"} + + def test_request_extra_json(self) -> None: + request = self.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 = self.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 = self.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) -> None: + request = self.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 = self.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) -> None: + request = self.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 = self.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 = self.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: Browserbase) -> None: + request = client._build_request( + FinalRequestOptions.construct( + method="get", + 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) -> 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 = self.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) -> 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 = self.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 = self.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) -> 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 = self.client.get("/foo", cast_to=Model) + assert isinstance(response, Model) + 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 + ) + 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/" + + 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) + assert client.base_url == "http://localhost:5000/from/env/" + + @pytest.mark.parametrize( + "client", + [ + 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, + http_client=httpx.Client(), + ), + ], + ids=["standard", "custom http client"], + ) + def test_base_url_trailing_slash(self, client: Browserbase) -> None: + request = client._build_request( + FinalRequestOptions( + method="post", + url="/foo", + json_data={"foo": "bar"}, + ), + ) + assert request.url == "http://localhost:5000/custom/path/foo" + + @pytest.mark.parametrize( + "client", + [ + 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, + http_client=httpx.Client(), + ), + ], + ids=["standard", "custom http client"], + ) + def test_base_url_no_trailing_slash(self, client: Browserbase) -> None: + request = client._build_request( + FinalRequestOptions( + method="post", + url="/foo", + json_data={"foo": "bar"}, + ), + ) + assert request.url == "http://localhost:5000/custom/path/foo" + + @pytest.mark.parametrize( + "client", + [ + 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, + http_client=httpx.Client(), + ), + ], + ids=["standard", "custom http client"], + ) + def test_absolute_request_url(self, client: Browserbase) -> None: + request = client._build_request( + FinalRequestOptions( + method="post", + url="https://myapi.com/foo", + json_data={"foo": "bar"}, + ), + ) + assert request.url == "https://myapi.com/foo" + + def test_copied_client_does_not_close_http(self) -> None: + client = Browserbase(base_url=base_url, api_key=api_key, _strict_response_validation=True) + assert not client.is_closed() + + copied = client.copy() + assert copied is not client + + del copied + + assert not client.is_closed() + + def test_client_context_manager(self) -> None: + client = Browserbase(base_url=base_url, api_key=api_key, _strict_response_validation=True) + with client as c2: + assert c2 is client + assert not c2.is_closed() + assert not client.is_closed() + assert client.is_closed() + + @pytest.mark.respx(base_url=base_url) + def test_client_response_validation_error(self, respx_mock: MockRouter) -> 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: + self.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"): + 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: + class Model(BaseModel): + name: str + + 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) + + with pytest.raises(APIResponseValidationError): + strict_client.get("/foo", cast_to=Model) + + client = Browserbase(base_url=base_url, api_key=api_key, _strict_response_validation=False) + + response = client.get("/foo", cast_to=Model) + assert isinstance(response, str) # type: ignore[unreachable] + + @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, "", 7.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) -> None: + client = Browserbase(base_url=base_url, api_key=api_key, _strict_response_validation=True) + + 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("browserbase._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) -> None: + respx_mock.post("/v1/contexts").mock(side_effect=httpx.TimeoutException("Test timeout error")) + + with pytest.raises(APITimeoutError): + self.client.post( + "/v1/contexts", + body=cast(object, dict(project_id="projectId")), + cast_to=httpx.Response, + options={"headers": {RAW_RESPONSE_HEADER: "stream"}}, + ) + + assert _get_open_connections(self.client) == 0 + + @mock.patch("browserbase._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) -> None: + respx_mock.post("/v1/contexts").mock(return_value=httpx.Response(500)) + + with pytest.raises(APIStatusError): + self.client.post( + "/v1/contexts", + body=cast(object, dict(project_id="projectId")), + cast_to=httpx.Response, + options={"headers": {RAW_RESPONSE_HEADER: "stream"}}, + ) + + assert _get_open_connections(self.client) == 0 + + @pytest.mark.parametrize("failures_before_success", [0, 2, 4]) + @mock.patch("browserbase._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: Browserbase, + 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("/v1/contexts").mock(side_effect=retry_handler) + + response = client.contexts.with_raw_response.create(project_id="projectId") + + 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("browserbase._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 + ) -> 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("/v1/contexts").mock(side_effect=retry_handler) + + response = client.contexts.with_raw_response.create( + project_id="projectId", 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("browserbase._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 + ) -> 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("/v1/contexts").mock(side_effect=retry_handler) + + response = client.contexts.with_raw_response.create( + project_id="projectId", extra_headers={"x-stainless-retry-count": "42"} + ) + + assert response.http_request.headers.get("x-stainless-retry-count") == "42" + + +class TestAsyncBrowserbase: + client = AsyncBrowserbase(base_url=base_url, api_key=api_key, _strict_response_validation=True) + + @pytest.mark.respx(base_url=base_url) + @pytest.mark.asyncio + async def test_raw_response(self, respx_mock: MockRouter) -> None: + respx_mock.post("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"})) + + response = await self.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) + @pytest.mark.asyncio + async def test_raw_response_for_binary(self, respx_mock: MockRouter) -> None: + respx_mock.post("/foo").mock( + return_value=httpx.Response(200, headers={"Content-Type": "application/binary"}, content='{"foo": "bar"}') + ) + + response = await self.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) -> None: + copied = self.client.copy() + assert id(copied) != id(self.client) + + copied = self.client.copy(api_key="another My API Key") + assert copied.api_key == "another My API Key" + assert self.client.api_key == "My API Key" + + def test_copy_default_options(self) -> None: + # options that have a default are overridden correctly + copied = self.client.copy(max_retries=7) + assert copied.max_retries == 7 + assert self.client.max_retries == 2 + + copied2 = copied.copy(max_retries=6) + assert copied2.max_retries == 6 + assert copied.max_retries == 7 + + # timeout + assert isinstance(self.client.timeout, httpx.Timeout) + copied = self.client.copy(timeout=None) + assert copied.timeout is None + assert isinstance(self.client.timeout, httpx.Timeout) + + def test_copy_default_headers(self) -> None: + 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" + + # 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"}) + + def test_copy_default_query(self) -> None: + client = AsyncBrowserbase( + 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"}) + + def test_copy_signature(self) -> 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. + self.client.__init__, # type: ignore[misc] + ) + copy_signature = inspect.signature(self.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" + + def test_copy_build_request(self) -> None: + options = FinalRequestOptions(method="get", url="/foo") + + def build_request(options: FinalRequestOptions) -> None: + client = self.client.copy() + client._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. + "browserbase/_legacy_response.py", + "browserbase/_response.py", + # pydantic.BaseModel.model_dump || pydantic.BaseModel.dict leak memory for some reason. + "browserbase/_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) -> None: + request = self.client._build_request(FinalRequestOptions(method="get", url="/foo")) + timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore + assert timeout == DEFAULT_TIMEOUT + + request = self.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 = AsyncBrowserbase( + 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) + + 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( + 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) + + # no timeout given to the httpx client should not use the httpx default + async with httpx.AsyncClient() as http_client: + client = AsyncBrowserbase( + 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 + + # explicitly passing the default timeout currently results in it being ignored + async with httpx.AsyncClient(timeout=HTTPX_DEFAULT_TIMEOUT) as http_client: + client = AsyncBrowserbase( + 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 + + def test_invalid_http_client(self) -> None: + with pytest.raises(TypeError, match="Invalid `http_client` arg"): + with httpx.Client() as http_client: + AsyncBrowserbase( + 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: + client = AsyncBrowserbase( + base_url=base_url, api_key=api_key, _strict_response_validation=True, default_headers={"X-Foo": "bar"} + ) + request = client._build_request(FinalRequestOptions(method="get", url="/foo")) + assert request.headers.get("x-foo") == "bar" + assert request.headers.get("x-stainless-lang") == "python" + + client2 = AsyncBrowserbase( + base_url=base_url, + api_key=api_key, + _strict_response_validation=True, + default_headers={ + "X-Foo": "stainless", + "X-Stainless-Lang": "my-overriding-header", + }, + ) + request = 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" + + def test_validate_headers(self) -> None: + 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("X-BB-API-Key") == api_key + + with pytest.raises(BrowserbaseError): + with update_env(**{"BROWSERBASE_API_KEY": Omit()}): + client2 = AsyncBrowserbase(base_url=base_url, api_key=None, _strict_response_validation=True) + _ = client2 + + def test_default_query_option(self) -> None: + 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")) + 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": "overriden"}, + ) + ) + url = httpx.URL(request.url) + assert dict(url.params) == {"foo": "baz", "query_param": "overriden"} + + def test_request_extra_json(self) -> None: + request = self.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 = self.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 = self.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) -> None: + request = self.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 = self.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) -> None: + request = self.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 = self.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 = self.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: AsyncBrowserbase) -> None: + request = async_client._build_request( + FinalRequestOptions.construct( + method="get", + 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) -> 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 self.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) -> 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 self.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 self.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) -> 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 self.client.get("/foo", cast_to=Model) + assert isinstance(response, Model) + assert response.foo == 2 + + def test_base_url_setter(self) -> None: + 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/" + + client.base_url = "https://example.com/from_setter" # type: ignore[assignment] + + assert client.base_url == "https://example.com/from_setter/" + + 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) + assert client.base_url == "http://localhost:5000/from/env/" + + @pytest.mark.parametrize( + "client", + [ + AsyncBrowserbase( + base_url="http://localhost:5000/custom/path/", api_key=api_key, _strict_response_validation=True + ), + AsyncBrowserbase( + base_url="http://localhost:5000/custom/path/", + api_key=api_key, + _strict_response_validation=True, + http_client=httpx.AsyncClient(), + ), + ], + ids=["standard", "custom http client"], + ) + def test_base_url_trailing_slash(self, client: AsyncBrowserbase) -> None: + request = client._build_request( + FinalRequestOptions( + method="post", + url="/foo", + json_data={"foo": "bar"}, + ), + ) + assert request.url == "http://localhost:5000/custom/path/foo" + + @pytest.mark.parametrize( + "client", + [ + AsyncBrowserbase( + base_url="http://localhost:5000/custom/path/", api_key=api_key, _strict_response_validation=True + ), + AsyncBrowserbase( + base_url="http://localhost:5000/custom/path/", + api_key=api_key, + _strict_response_validation=True, + http_client=httpx.AsyncClient(), + ), + ], + ids=["standard", "custom http client"], + ) + def test_base_url_no_trailing_slash(self, client: AsyncBrowserbase) -> None: + request = client._build_request( + FinalRequestOptions( + method="post", + url="/foo", + json_data={"foo": "bar"}, + ), + ) + assert request.url == "http://localhost:5000/custom/path/foo" + + @pytest.mark.parametrize( + "client", + [ + AsyncBrowserbase( + base_url="http://localhost:5000/custom/path/", api_key=api_key, _strict_response_validation=True + ), + AsyncBrowserbase( + base_url="http://localhost:5000/custom/path/", + api_key=api_key, + _strict_response_validation=True, + http_client=httpx.AsyncClient(), + ), + ], + ids=["standard", "custom http client"], + ) + def test_absolute_request_url(self, client: AsyncBrowserbase) -> None: + request = client._build_request( + FinalRequestOptions( + method="post", + url="https://myapi.com/foo", + json_data={"foo": "bar"}, + ), + ) + assert request.url == "https://myapi.com/foo" + + async def test_copied_client_does_not_close_http(self) -> None: + client = AsyncBrowserbase(base_url=base_url, api_key=api_key, _strict_response_validation=True) + assert not client.is_closed() + + copied = client.copy() + assert copied is not client + + del copied + + await asyncio.sleep(0.2) + assert not client.is_closed() + + async def test_client_context_manager(self) -> None: + client = AsyncBrowserbase(base_url=base_url, api_key=api_key, _strict_response_validation=True) + async with client as c2: + assert c2 is client + assert not c2.is_closed() + assert not client.is_closed() + assert client.is_closed() + + @pytest.mark.respx(base_url=base_url) + @pytest.mark.asyncio + async def test_client_response_validation_error(self, respx_mock: MockRouter) -> 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 self.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"): + AsyncBrowserbase( + base_url=base_url, api_key=api_key, _strict_response_validation=True, max_retries=cast(Any, None) + ) + + @pytest.mark.respx(base_url=base_url) + @pytest.mark.asyncio + 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 = 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) + + client = AsyncBrowserbase(base_url=base_url, api_key=api_key, _strict_response_validation=False) + + response = await client.get("/foo", cast_to=Model) + assert isinstance(response, str) # type: ignore[unreachable] + + @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, "", 7.8], # test large number potentially overflowing + ], + ) + @mock.patch("time.time", mock.MagicMock(return_value=1696004797)) + @pytest.mark.asyncio + async def test_parse_retry_after_header(self, remaining_retries: int, retry_after: str, timeout: float) -> None: + client = AsyncBrowserbase(base_url=base_url, api_key=api_key, _strict_response_validation=True) + + 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("browserbase._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) -> None: + respx_mock.post("/v1/contexts").mock(side_effect=httpx.TimeoutException("Test timeout error")) + + with pytest.raises(APITimeoutError): + await self.client.post( + "/v1/contexts", + body=cast(object, dict(project_id="projectId")), + cast_to=httpx.Response, + options={"headers": {RAW_RESPONSE_HEADER: "stream"}}, + ) + + assert _get_open_connections(self.client) == 0 + + @mock.patch("browserbase._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) -> None: + respx_mock.post("/v1/contexts").mock(return_value=httpx.Response(500)) + + with pytest.raises(APIStatusError): + await self.client.post( + "/v1/contexts", + body=cast(object, dict(project_id="projectId")), + cast_to=httpx.Response, + options={"headers": {RAW_RESPONSE_HEADER: "stream"}}, + ) + + assert _get_open_connections(self.client) == 0 + + @pytest.mark.parametrize("failures_before_success", [0, 2, 4]) + @mock.patch("browserbase._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) + @pytest.mark.respx(base_url=base_url) + @pytest.mark.asyncio + @pytest.mark.parametrize("failure_mode", ["status", "exception"]) + async def test_retries_taken( + self, + async_client: AsyncBrowserbase, + 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("/v1/contexts").mock(side_effect=retry_handler) + + response = await client.contexts.with_raw_response.create(project_id="projectId") + + 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("browserbase._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) + @pytest.mark.respx(base_url=base_url) + @pytest.mark.asyncio + async def test_omit_retry_count_header( + self, async_client: AsyncBrowserbase, 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("/v1/contexts").mock(side_effect=retry_handler) + + response = await client.contexts.with_raw_response.create( + project_id="projectId", 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("browserbase._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) + @pytest.mark.respx(base_url=base_url) + @pytest.mark.asyncio + async def test_overwrite_retry_count_header( + self, async_client: AsyncBrowserbase, 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("/v1/contexts").mock(side_effect=retry_handler) + + response = await client.contexts.with_raw_response.create( + project_id="projectId", extra_headers={"x-stainless-retry-count": "42"} + ) + + assert response.http_request.headers.get("x-stainless-retry-count") == "42" diff --git a/tests/test_deepcopy.py b/tests/test_deepcopy.py new file mode 100644 index 00000000..a2a29e38 --- /dev/null +++ b/tests/test_deepcopy.py @@ -0,0 +1,58 @@ +from browserbase._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..3c0fcb36 --- /dev/null +++ b/tests/test_extract_files.py @@ -0,0 +1,64 @@ +from __future__ import annotations + +from typing import Sequence + +import pytest + +from browserbase._types import FileTypes +from browserbase._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..d8842d61 --- /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 browserbase._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..5b8044f0 --- /dev/null +++ b/tests/test_models.py @@ -0,0 +1,829 @@ +import json +from typing import Any, Dict, List, Union, Optional, cast +from datetime import datetime, timezone +from typing_extensions import Literal, Annotated + +import pytest +import pydantic +from pydantic import Field + +from browserbase._utils import PropertyInfo +from browserbase._compat import PYDANTIC_V2, parse_obj, model_dump, model_json +from browserbase._models import 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_V2: + assert isinstance(m.foo, Submodel1) + assert m.foo.name == 3 # type: ignore + else: + assert isinstance(m.foo, Submodel2) + assert m.foo.name == "3" + + +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_V2: + expected_json = '{"created_at":"2019-12-27T18:11:19.117000Z"}' + else: + expected_json = '{"created_at": "2019-12-27T18:11:19.117000+00:00"}' + + 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 "resource_id" not in m.model_fields_set + + m = Model.construct(resource_id=None) + assert "resource_id" in m.model_fields_set + + m = Model.construct(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) == {} + + if PYDANTIC_V2: + + 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} + else: + with pytest.raises(ValueError, match="mode is only supported in Pydantic v2"): + m.to_dict(mode="json") + + 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 not PYDANTIC_V2: + with pytest.raises(ValueError, match="mode is only supported in Pydantic v2"): + m.model_dump(mode="json") + + 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_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_V2: + 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 not PYDANTIC_V2: + 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 not PYDANTIC_V2: + 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_V2: + assert m.data == 100 # type: ignore[comparison-overlap] + else: + # pydantic v1 automatically converts inputs to strings + # if the expected type is a str + assert m.data == "100" + + +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_V2: + assert m.data == 100 # type: ignore[comparison-overlap] + else: + # pydantic v1 automatically converts inputs to strings + # if the expected type is a str + assert m.data == "100" + + +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 hasattr(UnionType, "__discriminator__") + + 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 = UnionType.__discriminator__ + 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 UnionType.__discriminator__ is discriminator diff --git a/tests/test_qs.py b/tests/test_qs.py new file mode 100644 index 00000000..b6d53dc8 --- /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 browserbase._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..e2751d36 --- /dev/null +++ b/tests/test_required_args.py @@ -0,0 +1,111 @@ +from __future__ import annotations + +import pytest + +from browserbase._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..41e1a362 --- /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 browserbase import BaseModel, Browserbase, AsyncBrowserbase +from browserbase._response import ( + APIResponse, + BaseAPIResponse, + AsyncAPIResponse, + BinaryAPIResponse, + AsyncBinaryAPIResponse, + extract_response_type, +) +from browserbase._streaming import Stream +from browserbase._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: Browserbase) -> 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 browserbase import BaseModel`", + ): + response.parse(to=PydanticModel) + + +@pytest.mark.asyncio +async def test_async_response_parse_mismatched_basemodel(async_client: AsyncBrowserbase) -> 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 browserbase import BaseModel`", + ): + await response.parse(to=PydanticModel) + + +def test_response_parse_custom_stream(client: Browserbase) -> 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: AsyncBrowserbase) -> 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: Browserbase) -> 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: AsyncBrowserbase) -> 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: Browserbase) -> 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: AsyncBrowserbase) -> 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: Browserbase, 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: AsyncBrowserbase, 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: Browserbase) -> 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: AsyncBrowserbase) -> 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..fdd4969a --- /dev/null +++ b/tests/test_streaming.py @@ -0,0 +1,252 @@ +from __future__ import annotations + +from typing import Iterator, AsyncIterator + +import httpx +import pytest + +from browserbase import Browserbase, AsyncBrowserbase +from browserbase._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: + 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: Browserbase, async_client: AsyncBrowserbase) -> 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: Browserbase, async_client: AsyncBrowserbase) -> 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: Browserbase, async_client: AsyncBrowserbase) -> 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: Browserbase, async_client: AsyncBrowserbase) -> 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: Browserbase, async_client: AsyncBrowserbase +) -> 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: Browserbase, async_client: AsyncBrowserbase +) -> 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: Browserbase, async_client: AsyncBrowserbase) -> 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: Browserbase, + async_client: AsyncBrowserbase, +) -> 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: Browserbase, + async_client: AsyncBrowserbase, +) -> 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: 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() + + 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..436b8185 --- /dev/null +++ b/tests/test_transform.py @@ -0,0 +1,410 @@ +from __future__ import annotations + +import io +import pathlib +from typing import Any, List, Union, TypeVar, Iterable, Optional, cast +from datetime import date, datetime +from typing_extensions import Required, Annotated, TypedDict + +import pytest + +from browserbase._types import Base64FileInput +from browserbase._utils import ( + PropertyInfo, + transform as _transform, + parse_datetime, + async_transform as _async_transform, +) +from browserbase._compat import PYDANTIC_V2 +from browserbase._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")] + + +@parametrize +@pytest.mark.asyncio +async def test_iso8601_format(use_async: bool) -> None: + dt = datetime.fromisoformat("2023-02-23T14:16:36.337692+00:00") + assert await transform({"foo": dt}, DatetimeDict, use_async) == {"foo": "2023-02-23T14:16:36.337692+00:00"} # 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({"foo": None}, DateDict, use_async) == {"foo": None} # type: ignore[comparison-overlap] + assert await transform({"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_V2: + with pytest.warns(UserWarning): + params = await transform(model, Any, use_async) + else: + 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_V2: + with pytest.warns(UserWarning): + params = await transform(model, Any, use_async) + else: + 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"}] + } + + +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] diff --git a/tests/test_utils/test_proxy.py b/tests/test_utils/test_proxy.py new file mode 100644 index 00000000..986bef9d --- /dev/null +++ b/tests/test_utils/test_proxy.py @@ -0,0 +1,23 @@ +import operator +from typing import Any +from typing_extensions import override + +from browserbase._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" diff --git a/tests/test_utils/test_typing.py b/tests/test_utils/test_typing.py new file mode 100644 index 00000000..1516c978 --- /dev/null +++ b/tests/test_utils/test_typing.py @@ -0,0 +1,73 @@ +from __future__ import annotations + +from typing import Generic, TypeVar, cast + +from browserbase._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..ad9be375 --- /dev/null +++ b/tests/utils.py @@ -0,0 +1,155 @@ +from __future__ import annotations + +import os +import inspect +import traceback +import contextlib +from typing import Any, TypeVar, Iterator, cast +from datetime import date, datetime +from typing_extensions import Literal, get_args, get_origin, assert_type + +from browserbase._types import Omit, NoneType +from browserbase._utils import ( + is_dict, + is_list, + is_list_type, + is_union_type, + extract_type_arg, + is_annotated_type, +) +from browserbase._compat import PYDANTIC_V2, field_outer_type, get_model_fields +from browserbase._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_V2: + allow_none = False + else: + # 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) + + 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: + # 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 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 f61eb3269bf18d43458c298d3f4b65f95dde497e Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 25 Oct 2024 00:19:52 +0000 Subject: [PATCH 002/330] chore: update SDK settings (#1) --- .github/workflows/publish-pypi.yml | 31 +++++++++ .github/workflows/release-doctor.yml | 21 ++++++ .release-please-manifest.json | 3 + CONTRIBUTING.md | 4 +- README.md | 10 +-- bin/check-release-environment | 21 ++++++ pyproject.toml | 6 +- release-please-config.json | 66 +++++++++++++++++++ src/browserbase/_version.py | 2 +- src/browserbase/resources/contexts.py | 8 +-- src/browserbase/resources/extensions.py | 8 +-- src/browserbase/resources/projects.py | 8 +-- .../resources/sessions/downloads.py | 8 +-- src/browserbase/resources/sessions/logs.py | 8 +-- .../resources/sessions/recording.py | 8 +-- .../resources/sessions/sessions.py | 8 +-- src/browserbase/resources/sessions/uploads.py | 8 +-- 17 files changed, 185 insertions(+), 43 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..db8cf944 --- /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/sdk-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.35.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..3e17e458 --- /dev/null +++ b/.github/workflows/release-doctor.yml @@ -0,0 +1,21 @@ +name: Release Doctor +on: + pull_request: + branches: + - main + workflow_dispatch: + +jobs: + release_doctor: + name: release doctor + runs-on: ubuntu-latest + if: github.repository == 'browserbase/sdk-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..c4762802 --- /dev/null +++ b/.release-please-manifest.json @@ -0,0 +1,3 @@ +{ + ".": "0.0.1-alpha.0" +} \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8f94874b..45a7298e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -63,7 +63,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/browserbase-python.git +$ pip install git+ssh://git@github.com/browserbase/sdk-python.git ``` Alternatively, you can build from source and install the wheel file: @@ -121,7 +121,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/browserbase-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/sdk-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 dc64ac26..9035de99 100644 --- a/README.md +++ b/README.md @@ -15,8 +15,8 @@ The REST API documentation can be found on [docs.browserbase.com](https://docs.b ## Installation ```sh -# install from this staging repo -pip install git+ssh://git@github.com/stainless-sdks/browserbase-python.git +# install from the production repo +pip install git+ssh://git@github.com/browserbase/sdk-python.git ``` > [!NOTE] @@ -218,9 +218,9 @@ context = response.parse() # get the object that `contexts.create()` would have print(context.id) ``` -These methods return an [`APIResponse`](https://github.com/stainless-sdks/browserbase-python/tree/main/src/browserbase/_response.py) object. +These methods return an [`APIResponse`](https://github.com/browserbase/sdk-python/tree/main/src/browserbase/_response.py) object. -The async client returns an [`AsyncAPIResponse`](https://github.com/stainless-sdks/browserbase-python/tree/main/src/browserbase/_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/sdk-python/tree/main/src/browserbase/_response.py) with the same structure, the only difference being `await`able methods for reading the response content. #### `.with_streaming_response` @@ -316,7 +316,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/browserbase-python/issues) with questions, bugs, or suggestions. +We are keen for your feedback; please open an [issue](https://www.github.com/browserbase/sdk-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..6ad04d35 --- /dev/null +++ b/bin/check-release-environment @@ -0,0 +1,21 @@ +#!/usr/bin/env bash + +errors=() + +if [ -z "${PYPI_TOKEN}" ]; then + errors+=("The BROWSERBASE_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 26391407..57482c9b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,8 +36,8 @@ classifiers = [ ] [project.urls] -Homepage = "https://github.com/stainless-sdks/browserbase-python" -Repository = "https://github.com/stainless-sdks/browserbase-python" +Homepage = "https://github.com/browserbase/sdk-python" +Repository = "https://github.com/browserbase/sdk-python" @@ -123,7 +123,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/browserbase-python/tree/main/\g<2>)' +replacement = '[\1](https://github.com/browserbase/sdk-python/tree/main/\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..062330d1 --- /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/browserbase/_version.py" + ] +} \ No newline at end of file diff --git a/src/browserbase/_version.py b/src/browserbase/_version.py index f8f37176..b01ee95f 100644 --- a/src/browserbase/_version.py +++ b/src/browserbase/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "browserbase" -__version__ = "0.0.1-alpha.0" +__version__ = "0.0.1-alpha.0" # x-release-please-version diff --git a/src/browserbase/resources/contexts.py b/src/browserbase/resources/contexts.py index 9d7ea73b..806cb012 100644 --- a/src/browserbase/resources/contexts.py +++ b/src/browserbase/resources/contexts.py @@ -33,7 +33,7 @@ def with_raw_response(self) -> ContextsResourceWithRawResponse: This property can be used as a prefix for any HTTP method call to return the the raw response object instead of the parsed content. - For more information, see https://www.github.com/stainless-sdks/browserbase-python#accessing-raw-response-data-eg-headers + For more information, see https://www.github.com/browserbase/sdk-python#accessing-raw-response-data-eg-headers """ return ContextsResourceWithRawResponse(self) @@ -42,7 +42,7 @@ def with_streaming_response(self) -> ContextsResourceWithStreamingResponse: """ An alternative to `.with_raw_response` that doesn't eagerly read the response body. - For more information, see https://www.github.com/stainless-sdks/browserbase-python#with_streaming_response + For more information, see https://www.github.com/browserbase/sdk-python#with_streaming_response """ return ContextsResourceWithStreamingResponse(self) @@ -156,7 +156,7 @@ def with_raw_response(self) -> AsyncContextsResourceWithRawResponse: This property can be used as a prefix for any HTTP method call to return the the raw response object instead of the parsed content. - For more information, see https://www.github.com/stainless-sdks/browserbase-python#accessing-raw-response-data-eg-headers + For more information, see https://www.github.com/browserbase/sdk-python#accessing-raw-response-data-eg-headers """ return AsyncContextsResourceWithRawResponse(self) @@ -165,7 +165,7 @@ def with_streaming_response(self) -> AsyncContextsResourceWithStreamingResponse: """ An alternative to `.with_raw_response` that doesn't eagerly read the response body. - For more information, see https://www.github.com/stainless-sdks/browserbase-python#with_streaming_response + For more information, see https://www.github.com/browserbase/sdk-python#with_streaming_response """ return AsyncContextsResourceWithStreamingResponse(self) diff --git a/src/browserbase/resources/extensions.py b/src/browserbase/resources/extensions.py index 199f3744..dc6c0ac7 100644 --- a/src/browserbase/resources/extensions.py +++ b/src/browserbase/resources/extensions.py @@ -35,7 +35,7 @@ def with_raw_response(self) -> ExtensionsResourceWithRawResponse: This property can be used as a prefix for any HTTP method call to return the the raw response object instead of the parsed content. - For more information, see https://www.github.com/stainless-sdks/browserbase-python#accessing-raw-response-data-eg-headers + For more information, see https://www.github.com/browserbase/sdk-python#accessing-raw-response-data-eg-headers """ return ExtensionsResourceWithRawResponse(self) @@ -44,7 +44,7 @@ def with_streaming_response(self) -> ExtensionsResourceWithStreamingResponse: """ An alternative to `.with_raw_response` that doesn't eagerly read the response body. - For more information, see https://www.github.com/stainless-sdks/browserbase-python#with_streaming_response + For more information, see https://www.github.com/browserbase/sdk-python#with_streaming_response """ return ExtensionsResourceWithStreamingResponse(self) @@ -162,7 +162,7 @@ def with_raw_response(self) -> AsyncExtensionsResourceWithRawResponse: This property can be used as a prefix for any HTTP method call to return the the raw response object instead of the parsed content. - For more information, see https://www.github.com/stainless-sdks/browserbase-python#accessing-raw-response-data-eg-headers + For more information, see https://www.github.com/browserbase/sdk-python#accessing-raw-response-data-eg-headers """ return AsyncExtensionsResourceWithRawResponse(self) @@ -171,7 +171,7 @@ def with_streaming_response(self) -> AsyncExtensionsResourceWithStreamingRespons """ An alternative to `.with_raw_response` that doesn't eagerly read the response body. - For more information, see https://www.github.com/stainless-sdks/browserbase-python#with_streaming_response + For more information, see https://www.github.com/browserbase/sdk-python#with_streaming_response """ return AsyncExtensionsResourceWithStreamingResponse(self) diff --git a/src/browserbase/resources/projects.py b/src/browserbase/resources/projects.py index 0d564b04..f8b1936a 100644 --- a/src/browserbase/resources/projects.py +++ b/src/browserbase/resources/projects.py @@ -28,7 +28,7 @@ def with_raw_response(self) -> ProjectsResourceWithRawResponse: This property can be used as a prefix for any HTTP method call to return the the raw response object instead of the parsed content. - For more information, see https://www.github.com/stainless-sdks/browserbase-python#accessing-raw-response-data-eg-headers + For more information, see https://www.github.com/browserbase/sdk-python#accessing-raw-response-data-eg-headers """ return ProjectsResourceWithRawResponse(self) @@ -37,7 +37,7 @@ def with_streaming_response(self) -> ProjectsResourceWithStreamingResponse: """ An alternative to `.with_raw_response` that doesn't eagerly read the response body. - For more information, see https://www.github.com/stainless-sdks/browserbase-python#with_streaming_response + For more information, see https://www.github.com/browserbase/sdk-python#with_streaming_response """ return ProjectsResourceWithStreamingResponse(self) @@ -134,7 +134,7 @@ def with_raw_response(self) -> AsyncProjectsResourceWithRawResponse: This property can be used as a prefix for any HTTP method call to return the the raw response object instead of the parsed content. - For more information, see https://www.github.com/stainless-sdks/browserbase-python#accessing-raw-response-data-eg-headers + For more information, see https://www.github.com/browserbase/sdk-python#accessing-raw-response-data-eg-headers """ return AsyncProjectsResourceWithRawResponse(self) @@ -143,7 +143,7 @@ def with_streaming_response(self) -> AsyncProjectsResourceWithStreamingResponse: """ An alternative to `.with_raw_response` that doesn't eagerly read the response body. - For more information, see https://www.github.com/stainless-sdks/browserbase-python#with_streaming_response + For more information, see https://www.github.com/browserbase/sdk-python#with_streaming_response """ return AsyncProjectsResourceWithStreamingResponse(self) diff --git a/src/browserbase/resources/sessions/downloads.py b/src/browserbase/resources/sessions/downloads.py index 1792aecc..5b60ffe3 100644 --- a/src/browserbase/resources/sessions/downloads.py +++ b/src/browserbase/resources/sessions/downloads.py @@ -29,7 +29,7 @@ def with_raw_response(self) -> DownloadsResourceWithRawResponse: This property can be used as a prefix for any HTTP method call to return the the raw response object instead of the parsed content. - For more information, see https://www.github.com/stainless-sdks/browserbase-python#accessing-raw-response-data-eg-headers + For more information, see https://www.github.com/browserbase/sdk-python#accessing-raw-response-data-eg-headers """ return DownloadsResourceWithRawResponse(self) @@ -38,7 +38,7 @@ def with_streaming_response(self) -> DownloadsResourceWithStreamingResponse: """ An alternative to `.with_raw_response` that doesn't eagerly read the response body. - For more information, see https://www.github.com/stainless-sdks/browserbase-python#with_streaming_response + For more information, see https://www.github.com/browserbase/sdk-python#with_streaming_response """ return DownloadsResourceWithStreamingResponse(self) @@ -84,7 +84,7 @@ def with_raw_response(self) -> AsyncDownloadsResourceWithRawResponse: This property can be used as a prefix for any HTTP method call to return the the raw response object instead of the parsed content. - For more information, see https://www.github.com/stainless-sdks/browserbase-python#accessing-raw-response-data-eg-headers + For more information, see https://www.github.com/browserbase/sdk-python#accessing-raw-response-data-eg-headers """ return AsyncDownloadsResourceWithRawResponse(self) @@ -93,7 +93,7 @@ def with_streaming_response(self) -> AsyncDownloadsResourceWithStreamingResponse """ An alternative to `.with_raw_response` that doesn't eagerly read the response body. - For more information, see https://www.github.com/stainless-sdks/browserbase-python#with_streaming_response + For more information, see https://www.github.com/browserbase/sdk-python#with_streaming_response """ return AsyncDownloadsResourceWithStreamingResponse(self) diff --git a/src/browserbase/resources/sessions/logs.py b/src/browserbase/resources/sessions/logs.py index 3ab61905..07fb5818 100644 --- a/src/browserbase/resources/sessions/logs.py +++ b/src/browserbase/resources/sessions/logs.py @@ -26,7 +26,7 @@ def with_raw_response(self) -> LogsResourceWithRawResponse: This property can be used as a prefix for any HTTP method call to return the the raw response object instead of the parsed content. - For more information, see https://www.github.com/stainless-sdks/browserbase-python#accessing-raw-response-data-eg-headers + For more information, see https://www.github.com/browserbase/sdk-python#accessing-raw-response-data-eg-headers """ return LogsResourceWithRawResponse(self) @@ -35,7 +35,7 @@ def with_streaming_response(self) -> LogsResourceWithStreamingResponse: """ An alternative to `.with_raw_response` that doesn't eagerly read the response body. - For more information, see https://www.github.com/stainless-sdks/browserbase-python#with_streaming_response + For more information, see https://www.github.com/browserbase/sdk-python#with_streaming_response """ return LogsResourceWithStreamingResponse(self) @@ -80,7 +80,7 @@ def with_raw_response(self) -> AsyncLogsResourceWithRawResponse: This property can be used as a prefix for any HTTP method call to return the the raw response object instead of the parsed content. - For more information, see https://www.github.com/stainless-sdks/browserbase-python#accessing-raw-response-data-eg-headers + For more information, see https://www.github.com/browserbase/sdk-python#accessing-raw-response-data-eg-headers """ return AsyncLogsResourceWithRawResponse(self) @@ -89,7 +89,7 @@ def with_streaming_response(self) -> AsyncLogsResourceWithStreamingResponse: """ An alternative to `.with_raw_response` that doesn't eagerly read the response body. - For more information, see https://www.github.com/stainless-sdks/browserbase-python#with_streaming_response + For more information, see https://www.github.com/browserbase/sdk-python#with_streaming_response """ return AsyncLogsResourceWithStreamingResponse(self) diff --git a/src/browserbase/resources/sessions/recording.py b/src/browserbase/resources/sessions/recording.py index 2904f527..b216fd9b 100644 --- a/src/browserbase/resources/sessions/recording.py +++ b/src/browserbase/resources/sessions/recording.py @@ -26,7 +26,7 @@ def with_raw_response(self) -> RecordingResourceWithRawResponse: This property can be used as a prefix for any HTTP method call to return the the raw response object instead of the parsed content. - For more information, see https://www.github.com/stainless-sdks/browserbase-python#accessing-raw-response-data-eg-headers + For more information, see https://www.github.com/browserbase/sdk-python#accessing-raw-response-data-eg-headers """ return RecordingResourceWithRawResponse(self) @@ -35,7 +35,7 @@ def with_streaming_response(self) -> RecordingResourceWithStreamingResponse: """ An alternative to `.with_raw_response` that doesn't eagerly read the response body. - For more information, see https://www.github.com/stainless-sdks/browserbase-python#with_streaming_response + For more information, see https://www.github.com/browserbase/sdk-python#with_streaming_response """ return RecordingResourceWithStreamingResponse(self) @@ -80,7 +80,7 @@ def with_raw_response(self) -> AsyncRecordingResourceWithRawResponse: This property can be used as a prefix for any HTTP method call to return the the raw response object instead of the parsed content. - For more information, see https://www.github.com/stainless-sdks/browserbase-python#accessing-raw-response-data-eg-headers + For more information, see https://www.github.com/browserbase/sdk-python#accessing-raw-response-data-eg-headers """ return AsyncRecordingResourceWithRawResponse(self) @@ -89,7 +89,7 @@ def with_streaming_response(self) -> AsyncRecordingResourceWithStreamingResponse """ An alternative to `.with_raw_response` that doesn't eagerly read the response body. - For more information, see https://www.github.com/stainless-sdks/browserbase-python#with_streaming_response + For more information, see https://www.github.com/browserbase/sdk-python#with_streaming_response """ return AsyncRecordingResourceWithStreamingResponse(self) diff --git a/src/browserbase/resources/sessions/sessions.py b/src/browserbase/resources/sessions/sessions.py index 6f6e10d1..f50cf0da 100644 --- a/src/browserbase/resources/sessions/sessions.py +++ b/src/browserbase/resources/sessions/sessions.py @@ -84,7 +84,7 @@ def with_raw_response(self) -> SessionsResourceWithRawResponse: This property can be used as a prefix for any HTTP method call to return the the raw response object instead of the parsed content. - For more information, see https://www.github.com/stainless-sdks/browserbase-python#accessing-raw-response-data-eg-headers + For more information, see https://www.github.com/browserbase/sdk-python#accessing-raw-response-data-eg-headers """ return SessionsResourceWithRawResponse(self) @@ -93,7 +93,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/browserbase-python#with_streaming_response + For more information, see https://www.github.com/browserbase/sdk-python#with_streaming_response """ return SessionsResourceWithStreamingResponse(self) @@ -334,7 +334,7 @@ def with_raw_response(self) -> AsyncSessionsResourceWithRawResponse: This property can be used as a prefix for any HTTP method call to return the the raw response object instead of the parsed content. - For more information, see https://www.github.com/stainless-sdks/browserbase-python#accessing-raw-response-data-eg-headers + For more information, see https://www.github.com/browserbase/sdk-python#accessing-raw-response-data-eg-headers """ return AsyncSessionsResourceWithRawResponse(self) @@ -343,7 +343,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/browserbase-python#with_streaming_response + For more information, see https://www.github.com/browserbase/sdk-python#with_streaming_response """ return AsyncSessionsResourceWithStreamingResponse(self) diff --git a/src/browserbase/resources/sessions/uploads.py b/src/browserbase/resources/sessions/uploads.py index f55820ba..e985e4d9 100644 --- a/src/browserbase/resources/sessions/uploads.py +++ b/src/browserbase/resources/sessions/uploads.py @@ -35,7 +35,7 @@ def with_raw_response(self) -> UploadsResourceWithRawResponse: This property can be used as a prefix for any HTTP method call to return the the raw response object instead of the parsed content. - For more information, see https://www.github.com/stainless-sdks/browserbase-python#accessing-raw-response-data-eg-headers + For more information, see https://www.github.com/browserbase/sdk-python#accessing-raw-response-data-eg-headers """ return UploadsResourceWithRawResponse(self) @@ -44,7 +44,7 @@ def with_streaming_response(self) -> UploadsResourceWithStreamingResponse: """ An alternative to `.with_raw_response` that doesn't eagerly read the response body. - For more information, see https://www.github.com/stainless-sdks/browserbase-python#with_streaming_response + For more information, see https://www.github.com/browserbase/sdk-python#with_streaming_response """ return UploadsResourceWithStreamingResponse(self) @@ -98,7 +98,7 @@ def with_raw_response(self) -> AsyncUploadsResourceWithRawResponse: This property can be used as a prefix for any HTTP method call to return the the raw response object instead of the parsed content. - For more information, see https://www.github.com/stainless-sdks/browserbase-python#accessing-raw-response-data-eg-headers + For more information, see https://www.github.com/browserbase/sdk-python#accessing-raw-response-data-eg-headers """ return AsyncUploadsResourceWithRawResponse(self) @@ -107,7 +107,7 @@ def with_streaming_response(self) -> AsyncUploadsResourceWithStreamingResponse: """ An alternative to `.with_raw_response` that doesn't eagerly read the response body. - For more information, see https://www.github.com/stainless-sdks/browserbase-python#with_streaming_response + For more information, see https://www.github.com/browserbase/sdk-python#with_streaming_response """ return AsyncUploadsResourceWithStreamingResponse(self) From 94865dd97a6957eab788c2211bbc3fdbcd56399c Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 25 Oct 2024 19:25:38 +0000 Subject: [PATCH 003/330] chore(internal): version bump (#3) --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/browserbase/_version.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index c4762802..55f722d9 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.0.1-alpha.0" + ".": "0.0.1-alpha.1" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 57482c9b..15a6e91b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "browserbase" -version = "0.0.1-alpha.0" +version = "0.0.1-alpha.1" description = "The official Python library for the browserbase API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/browserbase/_version.py b/src/browserbase/_version.py index b01ee95f..d933153e 100644 --- a/src/browserbase/_version.py +++ b/src/browserbase/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "browserbase" -__version__ = "0.0.1-alpha.0" # x-release-please-version +__version__ = "0.0.1-alpha.1" # x-release-please-version From ff5d515a3cd553e5973a5b411ad4f4a292d291fa Mon Sep 17 00:00:00 2001 From: stainless-bot Date: Fri, 25 Oct 2024 23:08:01 +0000 Subject: [PATCH 004/330] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index e74d0eed..5c7b4fcb 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,2 +1,2 @@ configured_endpoints: 18 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fbrowserbase-099e8b99a50c73a107fe278d9d286dca1cc4b26769aa223ea1bcf9924ba38467.yml +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fbrowserbase-0069ed71133ac7b0add07abd8562396c4b8e3c9a212e14a7586782eeed2ff373.yml From b332e367924f3c60eea1b06804c381709a2d5810 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 25 Oct 2024 23:47:11 +0000 Subject: [PATCH 005/330] feat(api): update via SDK Studio (#7) --- .stats.yml | 2 +- src/browserbase/types/session.py | 28 ++++++++++++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index 5c7b4fcb..e2d6b395 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,2 +1,2 @@ configured_endpoints: 18 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fbrowserbase-0069ed71133ac7b0add07abd8562396c4b8e3c9a212e14a7586782eeed2ff373.yml +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fbrowserbase-825ce446567db7e2dcda332131368fcaf1986bae2eff640205b4e1f7b582aaa4.yml diff --git a/src/browserbase/types/session.py b/src/browserbase/types/session.py index c8d7fe42..2bc16bb0 100644 --- a/src/browserbase/types/session.py +++ b/src/browserbase/types/session.py @@ -1,6 +1,8 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. +from typing import Optional from datetime import datetime +from typing_extensions import Literal from pydantic import Field as FieldInfo @@ -14,4 +16,30 @@ class Session(BaseModel): created_at: datetime = FieldInfo(alias="createdAt") + expires_at: datetime = FieldInfo(alias="expiresAt") + + keep_alive: bool = FieldInfo(alias="keepAlive") + """Indicates if the Session was created to be kept alive upon disconnections""" + + project_id: str = FieldInfo(alias="projectId") + """The Project ID linked to the Session.""" + + proxy_bytes: int = FieldInfo(alias="proxyBytes") + """Bytes used via the [Proxy](/features/stealth-mode#proxies-and-residential-ips)""" + + started_at: datetime = FieldInfo(alias="startedAt") + + status: Literal["RUNNING", "ERROR", "TIMED_OUT", "COMPLETED"] + updated_at: datetime = FieldInfo(alias="updatedAt") + + avg_cpu_usage: Optional[int] = FieldInfo(alias="avgCpuUsage", default=None) + """CPU used by the Session""" + + context_id: Optional[str] = FieldInfo(alias="contextId", default=None) + """Optional. The Context linked to the Session.""" + + ended_at: Optional[datetime] = FieldInfo(alias="endedAt", default=None) + + memory_usage: Optional[int] = FieldInfo(alias="memoryUsage", default=None) + """Memory used by the Session""" From ed485ce6466821230a083600542b055c980417e3 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 25 Oct 2024 23:48:26 +0000 Subject: [PATCH 006/330] chore(internal): version bump (#8) --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/browserbase/_version.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 55f722d9..ba6c3483 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.0.1-alpha.1" + ".": "0.1.0-alpha.1" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 15a6e91b..e0a6c1f2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "browserbase" -version = "0.0.1-alpha.1" +version = "0.1.0-alpha.1" description = "The official Python library for the browserbase API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/browserbase/_version.py b/src/browserbase/_version.py index d933153e..f6d720fe 100644 --- a/src/browserbase/_version.py +++ b/src/browserbase/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "browserbase" -__version__ = "0.0.1-alpha.1" # x-release-please-version +__version__ = "0.1.0-alpha.1" # x-release-please-version From 9082e6f06350ee3b9e6599eb5ad25876e3cd4075 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 26 Oct 2024 02:01:04 +0000 Subject: [PATCH 007/330] feat(api): update via SDK Studio (#10) --- .stats.yml | 2 +- src/browserbase/types/session.py | 38 ++++++++++++++++++++++---------- 2 files changed, 27 insertions(+), 13 deletions(-) diff --git a/.stats.yml b/.stats.yml index e2d6b395..bb76cf5a 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,2 +1,2 @@ configured_endpoints: 18 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fbrowserbase-825ce446567db7e2dcda332131368fcaf1986bae2eff640205b4e1f7b582aaa4.yml +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fbrowserbase-208ded3468d1fbad85834462bced46e59d6cff963b347f9ba69c0b4fabe483c0.yml diff --git a/src/browserbase/types/session.py b/src/browserbase/types/session.py index 2bc16bb0..a6803a79 100644 --- a/src/browserbase/types/session.py +++ b/src/browserbase/types/session.py @@ -14,32 +14,46 @@ class Session(BaseModel): id: str - created_at: datetime = FieldInfo(alias="createdAt") + created_at: datetime - expires_at: datetime = FieldInfo(alias="expiresAt") + expires_at: datetime - keep_alive: bool = FieldInfo(alias="keepAlive") + keep_alive: bool """Indicates if the Session was created to be kept alive upon disconnections""" - project_id: str = FieldInfo(alias="projectId") + project_id: str """The Project ID linked to the Session.""" - proxy_bytes: int = FieldInfo(alias="proxyBytes") - """Bytes used via the [Proxy](/features/stealth-mode#proxies-and-residential-ips)""" + region: str - started_at: datetime = FieldInfo(alias="startedAt") + started_at: datetime status: Literal["RUNNING", "ERROR", "TIMED_OUT", "COMPLETED"] - updated_at: datetime = FieldInfo(alias="updatedAt") + updated_at: datetime - avg_cpu_usage: Optional[int] = FieldInfo(alias="avgCpuUsage", default=None) + avg_cpu_usage: Optional[int] = None """CPU used by the Session""" - context_id: Optional[str] = FieldInfo(alias="contextId", default=None) + connect_url: Optional[str] = FieldInfo(alias="connectUrl", default=None) + + context_id: Optional[str] = None """Optional. The Context linked to the Session.""" - ended_at: Optional[datetime] = FieldInfo(alias="endedAt", default=None) + ended_at: Optional[datetime] = None + + is_idle: Optional[bool] = None - memory_usage: Optional[int] = FieldInfo(alias="memoryUsage", default=None) + memory_usage: Optional[int] = None """Memory used by the Session""" + + proxy_bytes: Optional[int] = None + """Bytes used via the [Proxy](/features/stealth-mode#proxies-and-residential-ips)""" + + selenium_remote_url: Optional[str] = FieldInfo(alias="seleniumRemoteUrl", default=None) + + signing_key: Optional[str] = FieldInfo(alias="signingKey", default=None) + + viewport_height: Optional[int] = None + + viewport_width: Optional[int] = None From d67cbbcb57d3a2d1c5d947209ba0f2c747a8555e Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 26 Oct 2024 02:02:34 +0000 Subject: [PATCH 008/330] chore(internal): version bump (#11) --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/browserbase/_version.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index ba6c3483..f14b480a 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.1.0-alpha.1" + ".": "0.1.0-alpha.2" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index e0a6c1f2..a528fa75 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "browserbase" -version = "0.1.0-alpha.1" +version = "0.1.0-alpha.2" description = "The official Python library for the browserbase API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/browserbase/_version.py b/src/browserbase/_version.py index f6d720fe..16394629 100644 --- a/src/browserbase/_version.py +++ b/src/browserbase/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "browserbase" -__version__ = "0.1.0-alpha.1" # x-release-please-version +__version__ = "0.1.0-alpha.2" # x-release-please-version From a1ce7af5ffb1861c3ec4f7eca62f05d2c689fe25 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 26 Oct 2024 02:44:11 +0000 Subject: [PATCH 009/330] feat(api): update via SDK Studio (#13) --- .stats.yml | 2 +- src/browserbase/types/extension.py | 9 +++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/.stats.yml b/.stats.yml index bb76cf5a..b492b7b4 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,2 +1,2 @@ configured_endpoints: 18 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fbrowserbase-208ded3468d1fbad85834462bced46e59d6cff963b347f9ba69c0b4fabe483c0.yml +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fbrowserbase-9af27d5ca04efd55b732756ee4c81b76331e5ee8ab8c74576a3eaf16faac44f1.yml diff --git a/src/browserbase/types/extension.py b/src/browserbase/types/extension.py index da1a8b5c..018c55e2 100644 --- a/src/browserbase/types/extension.py +++ b/src/browserbase/types/extension.py @@ -12,6 +12,11 @@ class Extension(BaseModel): id: str - created_at: datetime = FieldInfo(alias="createdAt") + created_at: datetime - updated_at: datetime = FieldInfo(alias="updatedAt") + file_name: str = FieldInfo(alias="fileName") + + project_id: str = FieldInfo(alias="projectId") + """The Project ID linked to the uploaded Extension.""" + + updated_at: datetime From b8f2d6e200f68238389bd6530153faf3ac9fb06e Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 26 Oct 2024 02:46:39 +0000 Subject: [PATCH 010/330] chore(internal): version bump (#15) --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/browserbase/_version.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index f14b480a..aaf968a1 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.1.0-alpha.2" + ".": "0.1.0-alpha.3" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index a528fa75..2f455011 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "browserbase" -version = "0.1.0-alpha.2" +version = "0.1.0-alpha.3" description = "The official Python library for the browserbase API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/browserbase/_version.py b/src/browserbase/_version.py index 16394629..25f9355e 100644 --- a/src/browserbase/_version.py +++ b/src/browserbase/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "browserbase" -__version__ = "0.1.0-alpha.2" # x-release-please-version +__version__ = "0.1.0-alpha.3" # x-release-please-version From 2e54df1f9b6cc60300dc6bdd19a0d7d5b9fe367f Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sun, 27 Oct 2024 01:03:36 +0000 Subject: [PATCH 011/330] feat(api): update via SDK Studio (#16) --- .stats.yml | 2 +- .../resources/sessions/sessions.py | 6 +- .../types/session_create_params.py | 63 +------------------ 3 files changed, 7 insertions(+), 64 deletions(-) diff --git a/.stats.yml b/.stats.yml index b492b7b4..0e9ae696 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,2 +1,2 @@ configured_endpoints: 18 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fbrowserbase-9af27d5ca04efd55b732756ee4c81b76331e5ee8ab8c74576a3eaf16faac44f1.yml +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fbrowserbase-70bae250a6bae7dc6efc73ce837b3244eab63318b2d4de9a77ac8733e104df5b.yml diff --git a/src/browserbase/resources/sessions/sessions.py b/src/browserbase/resources/sessions/sessions.py index f50cf0da..d2c8d935 100644 --- a/src/browserbase/resources/sessions/sessions.py +++ b/src/browserbase/resources/sessions/sessions.py @@ -2,7 +2,7 @@ from __future__ import annotations -from typing import Union, Iterable +from typing import Union from typing_extensions import Literal import httpx @@ -104,7 +104,7 @@ def create( browser_settings: session_create_params.BrowserSettings | NotGiven = NOT_GIVEN, extension_id: str | NotGiven = NOT_GIVEN, keep_alive: bool | NotGiven = NOT_GIVEN, - proxies: Union[bool, Iterable[session_create_params.ProxiesUnionMember1]] | NotGiven = NOT_GIVEN, + proxies: Union[bool, object] | NotGiven = NOT_GIVEN, api_timeout: int | NotGiven = NOT_GIVEN, # 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. @@ -354,7 +354,7 @@ async def create( browser_settings: session_create_params.BrowserSettings | NotGiven = NOT_GIVEN, extension_id: str | NotGiven = NOT_GIVEN, keep_alive: bool | NotGiven = NOT_GIVEN, - proxies: Union[bool, Iterable[session_create_params.ProxiesUnionMember1]] | NotGiven = NOT_GIVEN, + proxies: Union[bool, object] | NotGiven = NOT_GIVEN, api_timeout: int | NotGiven = NOT_GIVEN, # 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. diff --git a/src/browserbase/types/session_create_params.py b/src/browserbase/types/session_create_params.py index fd43187f..add0212c 100644 --- a/src/browserbase/types/session_create_params.py +++ b/src/browserbase/types/session_create_params.py @@ -2,8 +2,8 @@ from __future__ import annotations -from typing import List, Union, Iterable -from typing_extensions import Literal, Required, Annotated, TypeAlias, TypedDict +from typing import List, Union +from typing_extensions import Literal, Required, Annotated, TypedDict from .._utils import PropertyInfo @@ -14,10 +14,6 @@ "BrowserSettingsFingerprint", "BrowserSettingsFingerprintScreen", "BrowserSettingsViewport", - "ProxiesUnionMember1", - "ProxiesUnionMember1BrowserbaseProxyConfig", - "ProxiesUnionMember1BrowserbaseProxyConfigGeolocation", - "ProxiesUnionMember1ExternalProxyConfig", ] @@ -42,7 +38,7 @@ class SessionCreateParams(TypedDict, total=False): This is available on the Startup plan only. """ - proxies: Union[bool, Iterable[ProxiesUnionMember1]] + proxies: Union[bool, object] """Proxy configuration. Can be true for default proxy, or an array of proxy configurations. @@ -131,56 +127,3 @@ class BrowserSettings(TypedDict, total=False): """Enable or disable captcha solving in the browser. Defaults to `true`.""" viewport: BrowserSettingsViewport - - -class ProxiesUnionMember1BrowserbaseProxyConfigGeolocation(TypedDict, total=False): - country: Required[str] - """Country code in ISO 3166-1 alpha-2 format""" - - city: str - """Name of the city. Use spaces for multi-word city names. Optional.""" - - state: str - """US state code (2 characters). Must also specify US as the country. Optional.""" - - -class ProxiesUnionMember1BrowserbaseProxyConfig(TypedDict, total=False): - type: Required[Literal["browserbase"]] - """Type of proxy. - - Always use 'browserbase' for the Browserbase managed proxy network. - """ - - domain_pattern: Annotated[str, PropertyInfo(alias="domainPattern")] - """Domain pattern for which this proxy should be used. - - If omitted, defaults to all domains. Optional. - """ - - geolocation: ProxiesUnionMember1BrowserbaseProxyConfigGeolocation - """Configuration for geolocation""" - - -class ProxiesUnionMember1ExternalProxyConfig(TypedDict, total=False): - server: Required[str] - """Server URL for external proxy. Required.""" - - type: Required[Literal["external"]] - """Type of proxy. Always 'external' for this config.""" - - domain_pattern: Annotated[str, PropertyInfo(alias="domainPattern")] - """Domain pattern for which this proxy should be used. - - If omitted, defaults to all domains. Optional. - """ - - password: str - """Password for external proxy authentication. Optional.""" - - username: str - """Username for external proxy authentication. Optional.""" - - -ProxiesUnionMember1: TypeAlias = Union[ - ProxiesUnionMember1BrowserbaseProxyConfig, ProxiesUnionMember1ExternalProxyConfig -] From 7c40daa45cf87276b3ee8dd08e6c39de7d637768 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sun, 27 Oct 2024 03:06:41 +0000 Subject: [PATCH 012/330] feat(api): update via SDK Studio (#18) --- .stats.yml | 2 +- src/browserbase/types/session.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.stats.yml b/.stats.yml index 0e9ae696..770eba6d 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,2 +1,2 @@ configured_endpoints: 18 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fbrowserbase-70bae250a6bae7dc6efc73ce837b3244eab63318b2d4de9a77ac8733e104df5b.yml +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fbrowserbase-69e3c041b63edae61bddbb624edc185621be0ad4b1355395616ce08bc8d74ef9.yml diff --git a/src/browserbase/types/session.py b/src/browserbase/types/session.py index a6803a79..ebc5c33e 100644 --- a/src/browserbase/types/session.py +++ b/src/browserbase/types/session.py @@ -24,6 +24,9 @@ class Session(BaseModel): project_id: str """The Project ID linked to the Session.""" + proxy_bytes: int = FieldInfo(alias="proxyBytes") + """Bytes used via the [Proxy](/features/stealth-mode#proxies-and-residential-ips)""" + region: str started_at: datetime @@ -47,9 +50,6 @@ class Session(BaseModel): memory_usage: Optional[int] = None """Memory used by the Session""" - proxy_bytes: Optional[int] = None - """Bytes used via the [Proxy](/features/stealth-mode#proxies-and-residential-ips)""" - selenium_remote_url: Optional[str] = FieldInfo(alias="seleniumRemoteUrl", default=None) signing_key: Optional[str] = FieldInfo(alias="signingKey", default=None) From 1ed061d1bf153d1499be88553cbb1a7f4fa9a2e7 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sun, 27 Oct 2024 03:09:07 +0000 Subject: [PATCH 013/330] feat(api): update via SDK Studio (#19) --- .stats.yml | 2 +- .../resources/sessions/sessions.py | 6 +- src/browserbase/types/extension.py | 9 +-- src/browserbase/types/session.py | 46 +------------- .../types/session_create_params.py | 63 ++++++++++++++++++- 5 files changed, 68 insertions(+), 58 deletions(-) diff --git a/.stats.yml b/.stats.yml index 770eba6d..5c7b4fcb 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,2 +1,2 @@ configured_endpoints: 18 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fbrowserbase-69e3c041b63edae61bddbb624edc185621be0ad4b1355395616ce08bc8d74ef9.yml +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fbrowserbase-0069ed71133ac7b0add07abd8562396c4b8e3c9a212e14a7586782eeed2ff373.yml diff --git a/src/browserbase/resources/sessions/sessions.py b/src/browserbase/resources/sessions/sessions.py index d2c8d935..f50cf0da 100644 --- a/src/browserbase/resources/sessions/sessions.py +++ b/src/browserbase/resources/sessions/sessions.py @@ -2,7 +2,7 @@ from __future__ import annotations -from typing import Union +from typing import Union, Iterable from typing_extensions import Literal import httpx @@ -104,7 +104,7 @@ def create( browser_settings: session_create_params.BrowserSettings | NotGiven = NOT_GIVEN, extension_id: str | NotGiven = NOT_GIVEN, keep_alive: bool | NotGiven = NOT_GIVEN, - proxies: Union[bool, object] | NotGiven = NOT_GIVEN, + proxies: Union[bool, Iterable[session_create_params.ProxiesUnionMember1]] | NotGiven = NOT_GIVEN, api_timeout: int | NotGiven = NOT_GIVEN, # 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. @@ -354,7 +354,7 @@ async def create( browser_settings: session_create_params.BrowserSettings | NotGiven = NOT_GIVEN, extension_id: str | NotGiven = NOT_GIVEN, keep_alive: bool | NotGiven = NOT_GIVEN, - proxies: Union[bool, object] | NotGiven = NOT_GIVEN, + proxies: Union[bool, Iterable[session_create_params.ProxiesUnionMember1]] | NotGiven = NOT_GIVEN, api_timeout: int | NotGiven = NOT_GIVEN, # 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. diff --git a/src/browserbase/types/extension.py b/src/browserbase/types/extension.py index 018c55e2..da1a8b5c 100644 --- a/src/browserbase/types/extension.py +++ b/src/browserbase/types/extension.py @@ -12,11 +12,6 @@ class Extension(BaseModel): id: str - created_at: datetime + created_at: datetime = FieldInfo(alias="createdAt") - file_name: str = FieldInfo(alias="fileName") - - project_id: str = FieldInfo(alias="projectId") - """The Project ID linked to the uploaded Extension.""" - - updated_at: datetime + updated_at: datetime = FieldInfo(alias="updatedAt") diff --git a/src/browserbase/types/session.py b/src/browserbase/types/session.py index ebc5c33e..c8d7fe42 100644 --- a/src/browserbase/types/session.py +++ b/src/browserbase/types/session.py @@ -1,8 +1,6 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -from typing import Optional from datetime import datetime -from typing_extensions import Literal from pydantic import Field as FieldInfo @@ -14,46 +12,6 @@ class Session(BaseModel): id: str - created_at: datetime + created_at: datetime = FieldInfo(alias="createdAt") - expires_at: datetime - - keep_alive: bool - """Indicates if the Session was created to be kept alive upon disconnections""" - - project_id: str - """The Project ID linked to the Session.""" - - proxy_bytes: int = FieldInfo(alias="proxyBytes") - """Bytes used via the [Proxy](/features/stealth-mode#proxies-and-residential-ips)""" - - region: str - - started_at: datetime - - status: Literal["RUNNING", "ERROR", "TIMED_OUT", "COMPLETED"] - - updated_at: datetime - - avg_cpu_usage: Optional[int] = None - """CPU used by the Session""" - - connect_url: Optional[str] = FieldInfo(alias="connectUrl", default=None) - - context_id: Optional[str] = None - """Optional. The Context linked to the Session.""" - - ended_at: Optional[datetime] = None - - is_idle: Optional[bool] = None - - memory_usage: Optional[int] = None - """Memory used by the Session""" - - selenium_remote_url: Optional[str] = FieldInfo(alias="seleniumRemoteUrl", default=None) - - signing_key: Optional[str] = FieldInfo(alias="signingKey", default=None) - - viewport_height: Optional[int] = None - - viewport_width: Optional[int] = None + updated_at: datetime = FieldInfo(alias="updatedAt") diff --git a/src/browserbase/types/session_create_params.py b/src/browserbase/types/session_create_params.py index add0212c..fd43187f 100644 --- a/src/browserbase/types/session_create_params.py +++ b/src/browserbase/types/session_create_params.py @@ -2,8 +2,8 @@ from __future__ import annotations -from typing import List, Union -from typing_extensions import Literal, Required, Annotated, TypedDict +from typing import List, Union, Iterable +from typing_extensions import Literal, Required, Annotated, TypeAlias, TypedDict from .._utils import PropertyInfo @@ -14,6 +14,10 @@ "BrowserSettingsFingerprint", "BrowserSettingsFingerprintScreen", "BrowserSettingsViewport", + "ProxiesUnionMember1", + "ProxiesUnionMember1BrowserbaseProxyConfig", + "ProxiesUnionMember1BrowserbaseProxyConfigGeolocation", + "ProxiesUnionMember1ExternalProxyConfig", ] @@ -38,7 +42,7 @@ class SessionCreateParams(TypedDict, total=False): This is available on the Startup plan only. """ - proxies: Union[bool, object] + proxies: Union[bool, Iterable[ProxiesUnionMember1]] """Proxy configuration. Can be true for default proxy, or an array of proxy configurations. @@ -127,3 +131,56 @@ class BrowserSettings(TypedDict, total=False): """Enable or disable captcha solving in the browser. Defaults to `true`.""" viewport: BrowserSettingsViewport + + +class ProxiesUnionMember1BrowserbaseProxyConfigGeolocation(TypedDict, total=False): + country: Required[str] + """Country code in ISO 3166-1 alpha-2 format""" + + city: str + """Name of the city. Use spaces for multi-word city names. Optional.""" + + state: str + """US state code (2 characters). Must also specify US as the country. Optional.""" + + +class ProxiesUnionMember1BrowserbaseProxyConfig(TypedDict, total=False): + type: Required[Literal["browserbase"]] + """Type of proxy. + + Always use 'browserbase' for the Browserbase managed proxy network. + """ + + domain_pattern: Annotated[str, PropertyInfo(alias="domainPattern")] + """Domain pattern for which this proxy should be used. + + If omitted, defaults to all domains. Optional. + """ + + geolocation: ProxiesUnionMember1BrowserbaseProxyConfigGeolocation + """Configuration for geolocation""" + + +class ProxiesUnionMember1ExternalProxyConfig(TypedDict, total=False): + server: Required[str] + """Server URL for external proxy. Required.""" + + type: Required[Literal["external"]] + """Type of proxy. Always 'external' for this config.""" + + domain_pattern: Annotated[str, PropertyInfo(alias="domainPattern")] + """Domain pattern for which this proxy should be used. + + If omitted, defaults to all domains. Optional. + """ + + password: str + """Password for external proxy authentication. Optional.""" + + username: str + """Username for external proxy authentication. Optional.""" + + +ProxiesUnionMember1: TypeAlias = Union[ + ProxiesUnionMember1BrowserbaseProxyConfig, ProxiesUnionMember1ExternalProxyConfig +] From 2cb6e9ad4cd5906d7b7ab7c930332a37e2b81134 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sun, 27 Oct 2024 03:27:31 +0000 Subject: [PATCH 014/330] feat(api): update via SDK Studio (#20) --- .stats.yml | 2 +- api.md | 4 +- .../resources/sessions/downloads.py | 4 +- .../resources/sessions/sessions.py | 22 ++++-- src/browserbase/types/__init__.py | 1 + .../types/session_create_params.py | 68 ++----------------- .../types/session_create_response.py | 57 ++++++++++++++++ tests/api_resources/test_sessions.py | 23 ++++--- 8 files changed, 98 insertions(+), 83 deletions(-) create mode 100644 src/browserbase/types/session_create_response.py diff --git a/.stats.yml b/.stats.yml index 5c7b4fcb..b9a6af2e 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,2 +1,2 @@ configured_endpoints: 18 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fbrowserbase-0069ed71133ac7b0add07abd8562396c4b8e3c9a212e14a7586782eeed2ff373.yml +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fbrowserbase-0d0ad7d4de2fa0b930b8d72fe6539ab248c7ed684b2e12b327b3bc0a466f9cde.yml diff --git a/api.md b/api.md index 40594f6d..3f21eb29 100644 --- a/api.md +++ b/api.md @@ -45,12 +45,12 @@ Methods: Types: ```python -from browserbase.types import Session, SessionLiveURLs, SessionListResponse +from browserbase.types import Session, SessionLiveURLs, SessionCreateResponse, SessionListResponse ``` Methods: -- client.sessions.create(\*\*params) -> Session +- client.sessions.create(\*\*params) -> SessionCreateResponse - client.sessions.retrieve(id) -> Session - client.sessions.update(id, \*\*params) -> Session - client.sessions.list(\*\*params) -> SessionListResponse diff --git a/src/browserbase/resources/sessions/downloads.py b/src/browserbase/resources/sessions/downloads.py index 5b60ffe3..461163b0 100644 --- a/src/browserbase/resources/sessions/downloads.py +++ b/src/browserbase/resources/sessions/downloads.py @@ -67,7 +67,7 @@ def list( """ if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") - extra_headers = {"Accept": "application/octet-stream", **(extra_headers or {})} + extra_headers = {"Accept": "application/zip", **(extra_headers or {})} return self._get( f"/v1/sessions/{id}/downloads", options=make_request_options( @@ -122,7 +122,7 @@ async def list( """ if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") - extra_headers = {"Accept": "application/octet-stream", **(extra_headers or {})} + extra_headers = {"Accept": "application/zip", **(extra_headers or {})} return await self._get( f"/v1/sessions/{id}/downloads", options=make_request_options( diff --git a/src/browserbase/resources/sessions/sessions.py b/src/browserbase/resources/sessions/sessions.py index f50cf0da..d298c011 100644 --- a/src/browserbase/resources/sessions/sessions.py +++ b/src/browserbase/resources/sessions/sessions.py @@ -2,7 +2,6 @@ from __future__ import annotations -from typing import Union, Iterable from typing_extensions import Literal import httpx @@ -57,6 +56,7 @@ from ...types.session import Session from ...types.session_live_urls import SessionLiveURLs from ...types.session_list_response import SessionListResponse +from ...types.session_create_response import SessionCreateResponse __all__ = ["SessionsResource", "AsyncSessionsResource"] @@ -104,7 +104,8 @@ def create( browser_settings: session_create_params.BrowserSettings | NotGiven = NOT_GIVEN, extension_id: str | NotGiven = NOT_GIVEN, keep_alive: bool | NotGiven = NOT_GIVEN, - proxies: Union[bool, Iterable[session_create_params.ProxiesUnionMember1]] | NotGiven = NOT_GIVEN, + proxies: object | NotGiven = NOT_GIVEN, + region: Literal["us-west-2", "us-east-1", "eu-central-1", "ap-southeast-1"] | NotGiven = NOT_GIVEN, api_timeout: int | NotGiven = NOT_GIVEN, # 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. @@ -112,7 +113,7 @@ def create( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> Session: + ) -> SessionCreateResponse: """Create a Session Args: @@ -130,6 +131,8 @@ def create( proxies: Proxy configuration. Can be true for default proxy, or an array of proxy configurations. + region: The region where the Session should run. + api_timeout: Duration in seconds after which the session will automatically end. Defaults to the Project's `defaultTimeout`. @@ -150,6 +153,7 @@ def create( "extension_id": extension_id, "keep_alive": keep_alive, "proxies": proxies, + "region": region, "timeout": api_timeout, }, session_create_params.SessionCreateParams, @@ -157,7 +161,7 @@ def create( options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=Session, + cast_to=SessionCreateResponse, ) def retrieve( @@ -354,7 +358,8 @@ async def create( browser_settings: session_create_params.BrowserSettings | NotGiven = NOT_GIVEN, extension_id: str | NotGiven = NOT_GIVEN, keep_alive: bool | NotGiven = NOT_GIVEN, - proxies: Union[bool, Iterable[session_create_params.ProxiesUnionMember1]] | NotGiven = NOT_GIVEN, + proxies: object | NotGiven = NOT_GIVEN, + region: Literal["us-west-2", "us-east-1", "eu-central-1", "ap-southeast-1"] | NotGiven = NOT_GIVEN, api_timeout: int | NotGiven = NOT_GIVEN, # 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. @@ -362,7 +367,7 @@ async def create( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> Session: + ) -> SessionCreateResponse: """Create a Session Args: @@ -380,6 +385,8 @@ async def create( proxies: Proxy configuration. Can be true for default proxy, or an array of proxy configurations. + region: The region where the Session should run. + api_timeout: Duration in seconds after which the session will automatically end. Defaults to the Project's `defaultTimeout`. @@ -400,6 +407,7 @@ async def create( "extension_id": extension_id, "keep_alive": keep_alive, "proxies": proxies, + "region": region, "timeout": api_timeout, }, session_create_params.SessionCreateParams, @@ -407,7 +415,7 @@ async def create( options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=Session, + cast_to=SessionCreateResponse, ) async def retrieve( diff --git a/src/browserbase/types/__init__.py b/src/browserbase/types/__init__.py index fcbbf0a0..ebc243db 100644 --- a/src/browserbase/types/__init__.py +++ b/src/browserbase/types/__init__.py @@ -17,3 +17,4 @@ from .context_create_response import ContextCreateResponse as ContextCreateResponse from .context_update_response import ContextUpdateResponse as ContextUpdateResponse from .extension_create_params import ExtensionCreateParams as ExtensionCreateParams +from .session_create_response import SessionCreateResponse as SessionCreateResponse diff --git a/src/browserbase/types/session_create_params.py b/src/browserbase/types/session_create_params.py index fd43187f..3b1920a8 100644 --- a/src/browserbase/types/session_create_params.py +++ b/src/browserbase/types/session_create_params.py @@ -2,8 +2,8 @@ from __future__ import annotations -from typing import List, Union, Iterable -from typing_extensions import Literal, Required, Annotated, TypeAlias, TypedDict +from typing import List +from typing_extensions import Literal, Required, Annotated, TypedDict from .._utils import PropertyInfo @@ -14,10 +14,6 @@ "BrowserSettingsFingerprint", "BrowserSettingsFingerprintScreen", "BrowserSettingsViewport", - "ProxiesUnionMember1", - "ProxiesUnionMember1BrowserbaseProxyConfig", - "ProxiesUnionMember1BrowserbaseProxyConfigGeolocation", - "ProxiesUnionMember1ExternalProxyConfig", ] @@ -42,12 +38,15 @@ class SessionCreateParams(TypedDict, total=False): This is available on the Startup plan only. """ - proxies: Union[bool, Iterable[ProxiesUnionMember1]] + proxies: object """Proxy configuration. Can be true for default proxy, or an array of proxy configurations. """ + region: Literal["us-west-2", "us-east-1", "eu-central-1", "ap-southeast-1"] + """The region where the Session should run.""" + api_timeout: Annotated[int, PropertyInfo(alias="timeout")] """Duration in seconds after which the session will automatically end. @@ -59,7 +58,7 @@ class BrowserSettingsContext(TypedDict, total=False): id: Required[str] """The Context ID.""" - persist: Required[bool] + persist: bool """Whether or not to persist the context after browsing. Defaults to `false`.""" @@ -131,56 +130,3 @@ class BrowserSettings(TypedDict, total=False): """Enable or disable captcha solving in the browser. Defaults to `true`.""" viewport: BrowserSettingsViewport - - -class ProxiesUnionMember1BrowserbaseProxyConfigGeolocation(TypedDict, total=False): - country: Required[str] - """Country code in ISO 3166-1 alpha-2 format""" - - city: str - """Name of the city. Use spaces for multi-word city names. Optional.""" - - state: str - """US state code (2 characters). Must also specify US as the country. Optional.""" - - -class ProxiesUnionMember1BrowserbaseProxyConfig(TypedDict, total=False): - type: Required[Literal["browserbase"]] - """Type of proxy. - - Always use 'browserbase' for the Browserbase managed proxy network. - """ - - domain_pattern: Annotated[str, PropertyInfo(alias="domainPattern")] - """Domain pattern for which this proxy should be used. - - If omitted, defaults to all domains. Optional. - """ - - geolocation: ProxiesUnionMember1BrowserbaseProxyConfigGeolocation - """Configuration for geolocation""" - - -class ProxiesUnionMember1ExternalProxyConfig(TypedDict, total=False): - server: Required[str] - """Server URL for external proxy. Required.""" - - type: Required[Literal["external"]] - """Type of proxy. Always 'external' for this config.""" - - domain_pattern: Annotated[str, PropertyInfo(alias="domainPattern")] - """Domain pattern for which this proxy should be used. - - If omitted, defaults to all domains. Optional. - """ - - password: str - """Password for external proxy authentication. Optional.""" - - username: str - """Username for external proxy authentication. Optional.""" - - -ProxiesUnionMember1: TypeAlias = Union[ - ProxiesUnionMember1BrowserbaseProxyConfig, ProxiesUnionMember1ExternalProxyConfig -] diff --git a/src/browserbase/types/session_create_response.py b/src/browserbase/types/session_create_response.py new file mode 100644 index 00000000..8c9ae097 --- /dev/null +++ b/src/browserbase/types/session_create_response.py @@ -0,0 +1,57 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional +from datetime import datetime +from typing_extensions import Literal + +from pydantic import Field as FieldInfo + +from .._models import BaseModel + +__all__ = ["SessionCreateResponse"] + + +class SessionCreateResponse(BaseModel): + id: str + + connect_url: str = FieldInfo(alias="connectUrl") + """WebSocket URL to connect to the Session.""" + + created_at: datetime = FieldInfo(alias="createdAt") + + expires_at: datetime = FieldInfo(alias="expiresAt") + + keep_alive: bool = FieldInfo(alias="keepAlive") + """Indicates if the Session was created to be kept alive upon disconnections""" + + project_id: str = FieldInfo(alias="projectId") + """The Project ID linked to the Session.""" + + proxy_bytes: int = FieldInfo(alias="proxyBytes") + """Bytes used via the [Proxy](/features/stealth-mode#proxies-and-residential-ips)""" + + region: Literal["us-west-2", "us-east-1", "eu-central-1", "ap-southeast-1"] + """The region where the Session is running.""" + + selenium_remote_url: str = FieldInfo(alias="seleniumRemoteUrl") + """HTTP URL to connect to the Session.""" + + signing_key: str = FieldInfo(alias="signingKey") + """Signing key to use when connecting to the Session via HTTP.""" + + started_at: datetime = FieldInfo(alias="startedAt") + + status: Literal["RUNNING", "ERROR", "TIMED_OUT", "COMPLETED"] + + updated_at: datetime = FieldInfo(alias="updatedAt") + + avg_cpu_usage: Optional[int] = FieldInfo(alias="avgCpuUsage", default=None) + """CPU used by the Session""" + + context_id: Optional[str] = FieldInfo(alias="contextId", default=None) + """Optional. The Context linked to the Session.""" + + ended_at: Optional[datetime] = FieldInfo(alias="endedAt", default=None) + + memory_usage: Optional[int] = FieldInfo(alias="memoryUsage", default=None) + """Memory used by the Session""" diff --git a/tests/api_resources/test_sessions.py b/tests/api_resources/test_sessions.py index 40b71737..dadadd46 100644 --- a/tests/api_resources/test_sessions.py +++ b/tests/api_resources/test_sessions.py @@ -13,6 +13,7 @@ Session, SessionLiveURLs, SessionListResponse, + SessionCreateResponse, ) base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") @@ -26,7 +27,7 @@ def test_method_create(self, client: Browserbase) -> None: session = client.sessions.create( project_id="projectId", ) - assert_matches_type(Session, session, path=["response"]) + assert_matches_type(SessionCreateResponse, session, path=["response"]) @parametrize def test_method_create_with_all_params(self, client: Browserbase) -> None: @@ -62,10 +63,11 @@ def test_method_create_with_all_params(self, client: Browserbase) -> None: }, extension_id="extensionId", keep_alive=True, - proxies=True, + proxies={}, + region="us-west-2", api_timeout=60, ) - assert_matches_type(Session, session, path=["response"]) + assert_matches_type(SessionCreateResponse, session, path=["response"]) @parametrize def test_raw_response_create(self, client: Browserbase) -> None: @@ -76,7 +78,7 @@ def test_raw_response_create(self, client: Browserbase) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" session = response.parse() - assert_matches_type(Session, session, path=["response"]) + assert_matches_type(SessionCreateResponse, session, path=["response"]) @parametrize def test_streaming_response_create(self, client: Browserbase) -> None: @@ -87,7 +89,7 @@ def test_streaming_response_create(self, client: Browserbase) -> None: assert response.http_request.headers.get("X-Stainless-Lang") == "python" session = response.parse() - assert_matches_type(Session, session, path=["response"]) + assert_matches_type(SessionCreateResponse, session, path=["response"]) assert cast(Any, response.is_closed) is True @@ -254,7 +256,7 @@ async def test_method_create(self, async_client: AsyncBrowserbase) -> None: session = await async_client.sessions.create( project_id="projectId", ) - assert_matches_type(Session, session, path=["response"]) + assert_matches_type(SessionCreateResponse, session, path=["response"]) @parametrize async def test_method_create_with_all_params(self, async_client: AsyncBrowserbase) -> None: @@ -290,10 +292,11 @@ async def test_method_create_with_all_params(self, async_client: AsyncBrowserbas }, extension_id="extensionId", keep_alive=True, - proxies=True, + proxies={}, + region="us-west-2", api_timeout=60, ) - assert_matches_type(Session, session, path=["response"]) + assert_matches_type(SessionCreateResponse, session, path=["response"]) @parametrize async def test_raw_response_create(self, async_client: AsyncBrowserbase) -> None: @@ -304,7 +307,7 @@ async def test_raw_response_create(self, async_client: AsyncBrowserbase) -> None assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" session = await response.parse() - assert_matches_type(Session, session, path=["response"]) + assert_matches_type(SessionCreateResponse, session, path=["response"]) @parametrize async def test_streaming_response_create(self, async_client: AsyncBrowserbase) -> None: @@ -315,7 +318,7 @@ async def test_streaming_response_create(self, async_client: AsyncBrowserbase) - assert response.http_request.headers.get("X-Stainless-Lang") == "python" session = await response.parse() - assert_matches_type(Session, session, path=["response"]) + assert_matches_type(SessionCreateResponse, session, path=["response"]) assert cast(Any, response.is_closed) is True From bfe05629029a65bea0eb3b10cd2ca26a43ef8025 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sun, 27 Oct 2024 03:30:22 +0000 Subject: [PATCH 015/330] feat(api): update via SDK Studio (#21) --- .stats.yml | 2 +- src/browserbase/types/context.py | 3 +++ src/browserbase/types/extension.py | 5 +++++ src/browserbase/types/project.py | 6 ++++++ src/browserbase/types/session.py | 31 ++++++++++++++++++++++++++++++ 5 files changed, 46 insertions(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index b9a6af2e..3d4a375d 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,2 +1,2 @@ configured_endpoints: 18 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fbrowserbase-0d0ad7d4de2fa0b930b8d72fe6539ab248c7ed684b2e12b327b3bc0a466f9cde.yml +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fbrowserbase-b37d85811d1ccbd73a7884f22792503aa7e3103d378c97c84028b8b3b79acddc.yml diff --git a/src/browserbase/types/context.py b/src/browserbase/types/context.py index ed5135b5..cb5c32fd 100644 --- a/src/browserbase/types/context.py +++ b/src/browserbase/types/context.py @@ -14,4 +14,7 @@ class Context(BaseModel): created_at: datetime = FieldInfo(alias="createdAt") + project_id: str = FieldInfo(alias="projectId") + """The Project ID linked to the uploaded Context.""" + updated_at: datetime = FieldInfo(alias="updatedAt") diff --git a/src/browserbase/types/extension.py b/src/browserbase/types/extension.py index da1a8b5c..94582c34 100644 --- a/src/browserbase/types/extension.py +++ b/src/browserbase/types/extension.py @@ -14,4 +14,9 @@ class Extension(BaseModel): created_at: datetime = FieldInfo(alias="createdAt") + file_name: str = FieldInfo(alias="fileName") + + project_id: str = FieldInfo(alias="projectId") + """The Project ID linked to the uploaded Extension.""" + updated_at: datetime = FieldInfo(alias="updatedAt") diff --git a/src/browserbase/types/project.py b/src/browserbase/types/project.py index cba2873f..afbcef63 100644 --- a/src/browserbase/types/project.py +++ b/src/browserbase/types/project.py @@ -14,4 +14,10 @@ class Project(BaseModel): created_at: datetime = FieldInfo(alias="createdAt") + default_timeout: int = FieldInfo(alias="defaultTimeout") + + name: str + + owner_id: str = FieldInfo(alias="ownerId") + updated_at: datetime = FieldInfo(alias="updatedAt") diff --git a/src/browserbase/types/session.py b/src/browserbase/types/session.py index c8d7fe42..8bd47f93 100644 --- a/src/browserbase/types/session.py +++ b/src/browserbase/types/session.py @@ -1,6 +1,8 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. +from typing import Optional from datetime import datetime +from typing_extensions import Literal from pydantic import Field as FieldInfo @@ -14,4 +16,33 @@ class Session(BaseModel): created_at: datetime = FieldInfo(alias="createdAt") + expires_at: datetime = FieldInfo(alias="expiresAt") + + keep_alive: bool = FieldInfo(alias="keepAlive") + """Indicates if the Session was created to be kept alive upon disconnections""" + + project_id: str = FieldInfo(alias="projectId") + """The Project ID linked to the Session.""" + + proxy_bytes: int = FieldInfo(alias="proxyBytes") + """Bytes used via the [Proxy](/features/stealth-mode#proxies-and-residential-ips)""" + + region: Literal["us-west-2", "us-east-1", "eu-central-1", "ap-southeast-1"] + """The region where the Session is running.""" + + started_at: datetime = FieldInfo(alias="startedAt") + + status: Literal["RUNNING", "ERROR", "TIMED_OUT", "COMPLETED"] + updated_at: datetime = FieldInfo(alias="updatedAt") + + avg_cpu_usage: Optional[int] = FieldInfo(alias="avgCpuUsage", default=None) + """CPU used by the Session""" + + context_id: Optional[str] = FieldInfo(alias="contextId", default=None) + """Optional. The Context linked to the Session.""" + + ended_at: Optional[datetime] = FieldInfo(alias="endedAt", default=None) + + memory_usage: Optional[int] = FieldInfo(alias="memoryUsage", default=None) + """Memory used by the Session""" From 93e3053c20fc5b9e4d35e1052a822359a55627a8 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sun, 27 Oct 2024 03:32:16 +0000 Subject: [PATCH 016/330] chore(internal): version bump (#22) --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/browserbase/_version.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index aaf968a1..b56c3d0b 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.1.0-alpha.3" + ".": "0.1.0-alpha.4" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 2f455011..382208a7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "browserbase" -version = "0.1.0-alpha.3" +version = "0.1.0-alpha.4" description = "The official Python library for the browserbase API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/browserbase/_version.py b/src/browserbase/_version.py index 25f9355e..17196336 100644 --- a/src/browserbase/_version.py +++ b/src/browserbase/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "browserbase" -__version__ = "0.1.0-alpha.3" # x-release-please-version +__version__ = "0.1.0-alpha.4" # x-release-please-version From 44f97329c5ae8d669c79851d40b98f685560a9d2 Mon Sep 17 00:00:00 2001 From: stainless-bot Date: Mon, 28 Oct 2024 01:14:35 +0000 Subject: [PATCH 017/330] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index 3d4a375d..743dc513 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,2 +1,2 @@ configured_endpoints: 18 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fbrowserbase-b37d85811d1ccbd73a7884f22792503aa7e3103d378c97c84028b8b3b79acddc.yml +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fbrowserbase-3140ed9942ce873c477c950cc9a13ccce88c3b054b903cdf78ac0db6cf2c634f.yml From a2c1c18f20e59bbc9d73aa521cc56d6d371a6cef Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 28 Oct 2024 01:18:34 +0000 Subject: [PATCH 018/330] feat(api): update via SDK Studio (#27) --- .stats.yml | 2 +- src/browserbase/resources/sessions/sessions.py | 10 ---------- src/browserbase/types/session_create_params.py | 6 ------ tests/api_resources/test_sessions.py | 2 -- 4 files changed, 1 insertion(+), 19 deletions(-) diff --git a/.stats.yml b/.stats.yml index 743dc513..b004f1be 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,2 +1,2 @@ configured_endpoints: 18 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fbrowserbase-3140ed9942ce873c477c950cc9a13ccce88c3b054b903cdf78ac0db6cf2c634f.yml +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fbrowserbase-1ccf843d2efab92c6fa01ffb11a0b5e6787218d750597687de526ac9f22f6b81.yml diff --git a/src/browserbase/resources/sessions/sessions.py b/src/browserbase/resources/sessions/sessions.py index d298c011..a2b83811 100644 --- a/src/browserbase/resources/sessions/sessions.py +++ b/src/browserbase/resources/sessions/sessions.py @@ -106,7 +106,6 @@ def create( keep_alive: bool | NotGiven = NOT_GIVEN, proxies: object | NotGiven = NOT_GIVEN, region: Literal["us-west-2", "us-east-1", "eu-central-1", "ap-southeast-1"] | NotGiven = NOT_GIVEN, - api_timeout: int | NotGiven = NOT_GIVEN, # 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, @@ -133,9 +132,6 @@ def create( region: The region where the Session should run. - api_timeout: Duration in seconds after which the session will automatically end. Defaults to - the Project's `defaultTimeout`. - extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -154,7 +150,6 @@ def create( "keep_alive": keep_alive, "proxies": proxies, "region": region, - "timeout": api_timeout, }, session_create_params.SessionCreateParams, ), @@ -360,7 +355,6 @@ async def create( keep_alive: bool | NotGiven = NOT_GIVEN, proxies: object | NotGiven = NOT_GIVEN, region: Literal["us-west-2", "us-east-1", "eu-central-1", "ap-southeast-1"] | NotGiven = NOT_GIVEN, - api_timeout: int | NotGiven = NOT_GIVEN, # 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, @@ -387,9 +381,6 @@ async def create( region: The region where the Session should run. - api_timeout: Duration in seconds after which the session will automatically end. Defaults to - the Project's `defaultTimeout`. - extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -408,7 +399,6 @@ async def create( "keep_alive": keep_alive, "proxies": proxies, "region": region, - "timeout": api_timeout, }, session_create_params.SessionCreateParams, ), diff --git a/src/browserbase/types/session_create_params.py b/src/browserbase/types/session_create_params.py index 3b1920a8..9b8d3b1e 100644 --- a/src/browserbase/types/session_create_params.py +++ b/src/browserbase/types/session_create_params.py @@ -47,12 +47,6 @@ class SessionCreateParams(TypedDict, total=False): region: Literal["us-west-2", "us-east-1", "eu-central-1", "ap-southeast-1"] """The region where the Session should run.""" - api_timeout: Annotated[int, PropertyInfo(alias="timeout")] - """Duration in seconds after which the session will automatically end. - - Defaults to the Project's `defaultTimeout`. - """ - class BrowserSettingsContext(TypedDict, total=False): id: Required[str] diff --git a/tests/api_resources/test_sessions.py b/tests/api_resources/test_sessions.py index dadadd46..81c0f79f 100644 --- a/tests/api_resources/test_sessions.py +++ b/tests/api_resources/test_sessions.py @@ -65,7 +65,6 @@ def test_method_create_with_all_params(self, client: Browserbase) -> None: keep_alive=True, proxies={}, region="us-west-2", - api_timeout=60, ) assert_matches_type(SessionCreateResponse, session, path=["response"]) @@ -294,7 +293,6 @@ async def test_method_create_with_all_params(self, async_client: AsyncBrowserbas keep_alive=True, proxies={}, region="us-west-2", - api_timeout=60, ) assert_matches_type(SessionCreateResponse, session, path=["response"]) From 61d49c0771b92fa082569d910bce90a1cf9975d0 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 28 Oct 2024 01:19:34 +0000 Subject: [PATCH 019/330] feat(api): update via SDK Studio (#28) --- .stats.yml | 2 +- src/browserbase/resources/sessions/sessions.py | 10 ++++++++++ src/browserbase/types/session_create_params.py | 6 ++++++ tests/api_resources/test_sessions.py | 2 ++ 4 files changed, 19 insertions(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index b004f1be..743dc513 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,2 +1,2 @@ configured_endpoints: 18 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fbrowserbase-1ccf843d2efab92c6fa01ffb11a0b5e6787218d750597687de526ac9f22f6b81.yml +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fbrowserbase-3140ed9942ce873c477c950cc9a13ccce88c3b054b903cdf78ac0db6cf2c634f.yml diff --git a/src/browserbase/resources/sessions/sessions.py b/src/browserbase/resources/sessions/sessions.py index a2b83811..d298c011 100644 --- a/src/browserbase/resources/sessions/sessions.py +++ b/src/browserbase/resources/sessions/sessions.py @@ -106,6 +106,7 @@ def create( keep_alive: bool | NotGiven = NOT_GIVEN, proxies: object | NotGiven = NOT_GIVEN, region: Literal["us-west-2", "us-east-1", "eu-central-1", "ap-southeast-1"] | NotGiven = NOT_GIVEN, + api_timeout: int | NotGiven = NOT_GIVEN, # 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, @@ -132,6 +133,9 @@ def create( region: The region where the Session should run. + api_timeout: Duration in seconds after which the session will automatically end. Defaults to + the Project's `defaultTimeout`. + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -150,6 +154,7 @@ def create( "keep_alive": keep_alive, "proxies": proxies, "region": region, + "timeout": api_timeout, }, session_create_params.SessionCreateParams, ), @@ -355,6 +360,7 @@ async def create( keep_alive: bool | NotGiven = NOT_GIVEN, proxies: object | NotGiven = NOT_GIVEN, region: Literal["us-west-2", "us-east-1", "eu-central-1", "ap-southeast-1"] | NotGiven = NOT_GIVEN, + api_timeout: int | NotGiven = NOT_GIVEN, # 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, @@ -381,6 +387,9 @@ async def create( region: The region where the Session should run. + api_timeout: Duration in seconds after which the session will automatically end. Defaults to + the Project's `defaultTimeout`. + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -399,6 +408,7 @@ async def create( "keep_alive": keep_alive, "proxies": proxies, "region": region, + "timeout": api_timeout, }, session_create_params.SessionCreateParams, ), diff --git a/src/browserbase/types/session_create_params.py b/src/browserbase/types/session_create_params.py index 9b8d3b1e..3b1920a8 100644 --- a/src/browserbase/types/session_create_params.py +++ b/src/browserbase/types/session_create_params.py @@ -47,6 +47,12 @@ class SessionCreateParams(TypedDict, total=False): region: Literal["us-west-2", "us-east-1", "eu-central-1", "ap-southeast-1"] """The region where the Session should run.""" + api_timeout: Annotated[int, PropertyInfo(alias="timeout")] + """Duration in seconds after which the session will automatically end. + + Defaults to the Project's `defaultTimeout`. + """ + class BrowserSettingsContext(TypedDict, total=False): id: Required[str] diff --git a/tests/api_resources/test_sessions.py b/tests/api_resources/test_sessions.py index 81c0f79f..dadadd46 100644 --- a/tests/api_resources/test_sessions.py +++ b/tests/api_resources/test_sessions.py @@ -65,6 +65,7 @@ def test_method_create_with_all_params(self, client: Browserbase) -> None: keep_alive=True, proxies={}, region="us-west-2", + api_timeout=60, ) assert_matches_type(SessionCreateResponse, session, path=["response"]) @@ -293,6 +294,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncBrowserbas keep_alive=True, proxies={}, region="us-west-2", + api_timeout=60, ) assert_matches_type(SessionCreateResponse, session, path=["response"]) From 72ad3221c989bfe6c55d6fa598778b342526ad2c Mon Sep 17 00:00:00 2001 From: stainless-bot Date: Mon, 28 Oct 2024 01:29:01 +0000 Subject: [PATCH 020/330] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index 743dc513..70bdddeb 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,2 +1,2 @@ configured_endpoints: 18 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fbrowserbase-3140ed9942ce873c477c950cc9a13ccce88c3b054b903cdf78ac0db6cf2c634f.yml +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fbrowserbase-60444f8b1aa1aa8dbec1e9f11e929c2b7ac27470764ef5f1796134fc27f3381c.yml From 056a4e541deb7974327d18cefd621d30ce0e2e68 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 28 Oct 2024 01:34:02 +0000 Subject: [PATCH 021/330] feat(api): update via SDK Studio (#29) --- src/browserbase/_client.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/browserbase/_client.py b/src/browserbase/_client.py index b0d011bb..df49eed2 100644 --- a/src/browserbase/_client.py +++ b/src/browserbase/_client.py @@ -94,7 +94,7 @@ def __init__( if base_url is None: base_url = os.environ.get("BROWSERBASE_BASE_URL") if base_url is None: - base_url = f"https://www.browserbase.com" + base_url = f"https://api.dev.browserbase.com" super().__init__( version=__version__, @@ -268,7 +268,7 @@ def __init__( if base_url is None: base_url = os.environ.get("BROWSERBASE_BASE_URL") if base_url is None: - base_url = f"https://www.browserbase.com" + base_url = f"https://api.dev.browserbase.com" super().__init__( version=__version__, From 47a6d0d29b2cc33f0c98a0d9e5ac23935c0ffbb8 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 28 Oct 2024 01:53:00 +0000 Subject: [PATCH 022/330] chore(internal): version bump (#30) --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/browserbase/_version.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index b56c3d0b..e8285b71 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.1.0-alpha.4" + ".": "0.1.0-alpha.5" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 382208a7..199c5610 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "browserbase" -version = "0.1.0-alpha.4" +version = "0.1.0-alpha.5" description = "The official Python library for the browserbase API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/browserbase/_version.py b/src/browserbase/_version.py index 17196336..1dd5a80e 100644 --- a/src/browserbase/_version.py +++ b/src/browserbase/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "browserbase" -__version__ = "0.1.0-alpha.4" # x-release-please-version +__version__ = "0.1.0-alpha.5" # x-release-please-version From d654e67d58367b4f2ece0ccaf2741f21e654a2c4 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 28 Oct 2024 21:21:24 +0000 Subject: [PATCH 023/330] feat(api): update via SDK Studio (#32) --- .stats.yml | 2 +- README.md | 4 ++ requirements-dev.lock | 21 ++++------ requirements.lock | 8 ++-- src/browserbase/__init__.py | 2 + src/browserbase/_client.py | 83 +++++++++++++++++++++++++++++++------ src/browserbase/_compat.py | 2 +- src/browserbase/_models.py | 10 ++--- src/browserbase/_types.py | 6 ++- tests/conftest.py | 14 ++++--- tests/test_client.py | 20 +++++++++ 11 files changed, 129 insertions(+), 43 deletions(-) diff --git a/.stats.yml b/.stats.yml index 70bdddeb..e5f4ae33 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,2 +1,2 @@ configured_endpoints: 18 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fbrowserbase-60444f8b1aa1aa8dbec1e9f11e929c2b7ac27470764ef5f1796134fc27f3381c.yml +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fbrowserbase-9f93c744538f57747ea1385817e21b40c318b65ebc155dca8950268beb280bc9.yml diff --git a/README.md b/README.md index 9035de99..a1703459 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,8 @@ from browserbase import Browserbase client = Browserbase( # This is the default and can be omitted api_key=os.environ.get("BROWSERBASE_API_KEY"), + # or 'production' | 'local'; defaults to "production". + environment="development", ) context = client.contexts.create( @@ -58,6 +60,8 @@ from browserbase import AsyncBrowserbase client = AsyncBrowserbase( # This is the default and can be omitted api_key=os.environ.get("BROWSERBASE_API_KEY"), + # or 'production' | 'local'; defaults to "production". + environment="development", ) diff --git a/requirements-dev.lock b/requirements-dev.lock index b6be61c0..20a4ebdc 100644 --- a/requirements-dev.lock +++ b/requirements-dev.lock @@ -16,8 +16,6 @@ anyio==4.4.0 # via httpx argcomplete==3.1.2 # via nox -attrs==23.1.0 - # via pytest certifi==2023.7.22 # via httpcore # via httpx @@ -28,8 +26,9 @@ distlib==0.3.7 # via virtualenv distro==1.8.0 # via browserbase -exceptiongroup==1.1.3 +exceptiongroup==1.2.2 # via anyio + # via pytest filelock==3.12.4 # via virtualenv h11==0.14.0 @@ -60,20 +59,18 @@ packaging==23.2 # via pytest platformdirs==3.11.0 # via virtualenv -pluggy==1.3.0 - # via pytest -py==1.11.0 +pluggy==1.5.0 # via pytest -pydantic==2.7.1 +pydantic==2.9.2 # via browserbase -pydantic-core==2.18.2 +pydantic-core==2.23.4 # via pydantic pygments==2.18.0 # via rich pyright==1.1.380 -pytest==7.1.1 +pytest==8.3.3 # via pytest-asyncio -pytest-asyncio==0.21.1 +pytest-asyncio==0.24.0 python-dateutil==2.8.2 # via time-machine pytz==2023.3.post1 @@ -90,10 +87,10 @@ sniffio==1.3.0 # via browserbase # via httpx time-machine==2.9.0 -tomli==2.0.1 +tomli==2.0.2 # via mypy # via pytest -typing-extensions==4.8.0 +typing-extensions==4.12.2 # via anyio # via browserbase # via mypy diff --git a/requirements.lock b/requirements.lock index 44b55d0d..5f5acc29 100644 --- a/requirements.lock +++ b/requirements.lock @@ -19,7 +19,7 @@ certifi==2023.7.22 # via httpx distro==1.8.0 # via browserbase -exceptiongroup==1.1.3 +exceptiongroup==1.2.2 # via anyio h11==0.14.0 # via httpcore @@ -30,15 +30,15 @@ httpx==0.25.2 idna==3.4 # via anyio # via httpx -pydantic==2.7.1 +pydantic==2.9.2 # via browserbase -pydantic-core==2.18.2 +pydantic-core==2.23.4 # via pydantic sniffio==1.3.0 # via anyio # via browserbase # via httpx -typing-extensions==4.8.0 +typing-extensions==4.12.2 # via anyio # via browserbase # via pydantic diff --git a/src/browserbase/__init__.py b/src/browserbase/__init__.py index 4b1d2804..ea5a5985 100644 --- a/src/browserbase/__init__.py +++ b/src/browserbase/__init__.py @@ -4,6 +4,7 @@ from ._types import NOT_GIVEN, NoneType, NotGiven, Transport, ProxiesTypes from ._utils import file_from_path from ._client import ( + ENVIRONMENTS, Client, Stream, Timeout, @@ -68,6 +69,7 @@ "AsyncStream", "Browserbase", "AsyncBrowserbase", + "ENVIRONMENTS", "file_from_path", "BaseModel", "DEFAULT_TIMEOUT", diff --git a/src/browserbase/_client.py b/src/browserbase/_client.py index df49eed2..461f9dae 100644 --- a/src/browserbase/_client.py +++ b/src/browserbase/_client.py @@ -3,8 +3,8 @@ from __future__ import annotations import os -from typing import Any, Union, Mapping -from typing_extensions import Self, override +from typing import Any, Dict, Union, Mapping, cast +from typing_extensions import Self, Literal, override import httpx @@ -33,6 +33,7 @@ ) __all__ = [ + "ENVIRONMENTS", "Timeout", "Transport", "ProxiesTypes", @@ -44,6 +45,12 @@ "AsyncClient", ] +ENVIRONMENTS: Dict[str, str] = { + "production": "https://api.browserbase.com", + "development": "https://api.dev.browserbase.com", + "local": "http://api.localhost", +} + class Browserbase(SyncAPIClient): contexts: resources.ContextsResource @@ -56,11 +63,14 @@ class Browserbase(SyncAPIClient): # client options api_key: str + _environment: Literal["production", "development", "local"] | NotGiven + def __init__( self, *, api_key: str | None = None, - base_url: str | httpx.URL | None = None, + environment: Literal["production", "development", "local"] | NotGiven = NOT_GIVEN, + base_url: str | httpx.URL | None | NotGiven = NOT_GIVEN, timeout: Union[float, Timeout, None, NotGiven] = NOT_GIVEN, max_retries: int = DEFAULT_MAX_RETRIES, default_headers: Mapping[str, str] | None = None, @@ -91,10 +101,31 @@ def __init__( ) self.api_key = api_key - if base_url is None: - base_url = os.environ.get("BROWSERBASE_BASE_URL") - if base_url is None: - base_url = f"https://api.dev.browserbase.com" + self._environment = environment + + 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 `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: + 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__, @@ -138,6 +169,7 @@ def copy( self, *, api_key: str | None = None, + environment: Literal["production", "development", "local"] | None = None, base_url: str | httpx.URL | None = None, timeout: float | Timeout | None | NotGiven = NOT_GIVEN, http_client: httpx.Client | None = None, @@ -173,6 +205,7 @@ def copy( 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, @@ -230,11 +263,14 @@ class AsyncBrowserbase(AsyncAPIClient): # client options api_key: str + _environment: Literal["production", "development", "local"] | NotGiven + def __init__( self, *, api_key: str | None = None, - base_url: str | httpx.URL | None = None, + environment: Literal["production", "development", "local"] | NotGiven = NOT_GIVEN, + base_url: str | httpx.URL | None | NotGiven = NOT_GIVEN, timeout: Union[float, Timeout, None, NotGiven] = NOT_GIVEN, max_retries: int = DEFAULT_MAX_RETRIES, default_headers: Mapping[str, str] | None = None, @@ -265,10 +301,31 @@ def __init__( ) self.api_key = api_key - if base_url is None: - base_url = os.environ.get("BROWSERBASE_BASE_URL") - if base_url is None: - base_url = f"https://api.dev.browserbase.com" + self._environment = environment + + 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 `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: + 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__, @@ -312,6 +369,7 @@ def copy( self, *, api_key: str | None = None, + environment: Literal["production", "development", "local"] | None = None, base_url: str | httpx.URL | None = None, timeout: float | Timeout | None | NotGiven = NOT_GIVEN, http_client: httpx.AsyncClient | None = None, @@ -347,6 +405,7 @@ def copy( 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, diff --git a/src/browserbase/_compat.py b/src/browserbase/_compat.py index 162a6fbe..d89920d9 100644 --- a/src/browserbase/_compat.py +++ b/src/browserbase/_compat.py @@ -133,7 +133,7 @@ def model_json(model: pydantic.BaseModel, *, indent: int | None = None) -> str: def model_dump( model: pydantic.BaseModel, *, - exclude: IncEx = None, + exclude: IncEx | None = None, exclude_unset: bool = False, exclude_defaults: bool = False, warnings: bool = True, diff --git a/src/browserbase/_models.py b/src/browserbase/_models.py index d386eaa3..42551b76 100644 --- a/src/browserbase/_models.py +++ b/src/browserbase/_models.py @@ -176,7 +176,7 @@ def __str__(self) -> str: # Based on https://github.com/samuelcolvin/pydantic/issues/1168#issuecomment-817742836. @classmethod @override - def construct( + def construct( # pyright: ignore[reportIncompatibleMethodOverride] cls: Type[ModelT], _fields_set: set[str] | None = None, **values: object, @@ -248,8 +248,8 @@ def model_dump( self, *, mode: Literal["json", "python"] | str = "python", - include: IncEx = None, - exclude: IncEx = None, + include: IncEx | None = None, + exclude: IncEx | None = None, by_alias: bool = False, exclude_unset: bool = False, exclude_defaults: bool = False, @@ -303,8 +303,8 @@ def model_dump_json( self, *, indent: int | None = None, - include: IncEx = None, - exclude: IncEx = None, + include: IncEx | None = None, + exclude: IncEx | None = None, by_alias: bool = False, exclude_unset: bool = False, exclude_defaults: bool = False, diff --git a/src/browserbase/_types.py b/src/browserbase/_types.py index 5ab69853..1691090d 100644 --- a/src/browserbase/_types.py +++ b/src/browserbase/_types.py @@ -16,7 +16,7 @@ Optional, Sequence, ) -from typing_extensions import Literal, Protocol, TypeAlias, TypedDict, override, runtime_checkable +from typing_extensions import Set, Literal, Protocol, TypeAlias, TypedDict, override, runtime_checkable import httpx import pydantic @@ -193,7 +193,9 @@ def get(self, __key: str) -> str | None: ... # Note: copied from Pydantic # https://github.com/pydantic/pydantic/blob/32ea570bf96e84234d2992e1ddf40ab8a565925a/pydantic/main.py#L49 -IncEx: TypeAlias = "set[int] | set[str] | dict[int, Any] | dict[str, Any] | None" +IncEx: TypeAlias = Union[ + Set[int], Set[str], Mapping[int, Union["IncEx", Literal[True]]], Mapping[str, Union["IncEx", Literal[True]]] +] PostParser = Callable[[Any], Any] diff --git a/tests/conftest.py b/tests/conftest.py index 06a9e893..15ddbcad 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,11 +1,11 @@ from __future__ import annotations import os -import asyncio import logging from typing import TYPE_CHECKING, Iterator, AsyncIterator import pytest +from pytest_asyncio import is_async_test from browserbase import Browserbase, AsyncBrowserbase @@ -17,11 +17,13 @@ logging.getLogger("browserbase").setLevel(logging.DEBUG) -@pytest.fixture(scope="session") -def event_loop() -> Iterator[asyncio.AbstractEventLoop]: - loop = asyncio.new_event_loop() - yield loop - loop.close() +# 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) base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") diff --git a/tests/test_client.py b/tests/test_client.py index fe8d7f86..9fe3b908 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -556,6 +556,16 @@ def test_base_url_env(self) -> None: 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(BROWSERBASE_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") + + client = Browserbase( + base_url=None, api_key=api_key, _strict_response_validation=True, environment="production" + ) + assert str(client.base_url).startswith("https://api.browserbase.com") + @pytest.mark.parametrize( "client", [ @@ -1332,6 +1342,16 @@ def test_base_url_env(self) -> None: 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(BROWSERBASE_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") + + client = AsyncBrowserbase( + base_url=None, api_key=api_key, _strict_response_validation=True, environment="production" + ) + assert str(client.base_url).startswith("https://api.browserbase.com") + @pytest.mark.parametrize( "client", [ From 24ddbf19737d05e8e6506a9641875220724552e4 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 28 Oct 2024 21:30:01 +0000 Subject: [PATCH 024/330] chore(internal): version bump (#34) --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/browserbase/_version.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index e8285b71..4f9005ea 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.1.0-alpha.5" + ".": "0.1.0-alpha.6" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 199c5610..cf85fb28 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "browserbase" -version = "0.1.0-alpha.5" +version = "0.1.0-alpha.6" description = "The official Python library for the browserbase API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/browserbase/_version.py b/src/browserbase/_version.py index 1dd5a80e..47f88c6c 100644 --- a/src/browserbase/_version.py +++ b/src/browserbase/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "browserbase" -__version__ = "0.1.0-alpha.5" # x-release-please-version +__version__ = "0.1.0-alpha.6" # x-release-please-version From c3fd039c818e07625eee7e53c21bb199da3d1d68 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 28 Oct 2024 23:14:15 +0000 Subject: [PATCH 025/330] feat(api): update via SDK Studio (#35) --- README.md | 47 ++++++++++---------- SECURITY.md | 2 +- pyproject.toml | 4 +- src/browserbase/__init__.py | 2 - src/browserbase/_client.py | 87 ++++++------------------------------- tests/test_client.py | 68 ++++++++++------------------- 6 files changed, 66 insertions(+), 144 deletions(-) diff --git a/README.md b/README.md index a1703459..53e20a98 100644 --- a/README.md +++ b/README.md @@ -33,14 +33,13 @@ from browserbase import Browserbase client = Browserbase( # This is the default and can be omitted api_key=os.environ.get("BROWSERBASE_API_KEY"), - # or 'production' | 'local'; defaults to "production". - environment="development", ) -context = client.contexts.create( - project_id="projectId", +session = client.sessions.create( + project_id="your_project_id", + proxies=True, ) -print(context.id) +print(session.id) ``` While you can provide an `api_key` keyword argument, @@ -60,16 +59,15 @@ from browserbase import AsyncBrowserbase client = AsyncBrowserbase( # This is the default and can be omitted api_key=os.environ.get("BROWSERBASE_API_KEY"), - # or 'production' | 'local'; defaults to "production". - environment="development", ) async def main() -> None: - context = await client.contexts.create( - project_id="projectId", + session = await client.sessions.create( + project_id="your_project_id", + proxies=True, ) - print(context.id) + print(session.id) asyncio.run(main()) @@ -102,8 +100,9 @@ from browserbase import Browserbase client = Browserbase() try: - client.contexts.create( - project_id="projectId", + client.sessions.create( + project_id="your_project_id", + proxies=True, ) except browserbase.APIConnectionError as e: print("The server could not be reached") @@ -147,8 +146,9 @@ client = Browserbase( ) # Or, configure per-request: -client.with_options(max_retries=5).contexts.create( - project_id="projectId", +client.with_options(max_retries=5).sessions.create( + project_id="your_project_id", + proxies=True, ) ``` @@ -172,8 +172,9 @@ client = Browserbase( ) # Override per-request: -client.with_options(timeout=5.0).contexts.create( - project_id="projectId", +client.with_options(timeout=5.0).sessions.create( + project_id="your_project_id", + proxies=True, ) ``` @@ -213,13 +214,14 @@ The "raw" Response object can be accessed by prefixing `.with_raw_response.` to from browserbase import Browserbase client = Browserbase() -response = client.contexts.with_raw_response.create( - project_id="projectId", +response = client.sessions.with_raw_response.create( + project_id="your_project_id", + proxies=True, ) print(response.headers.get('X-My-Header')) -context = response.parse() # get the object that `contexts.create()` would have returned -print(context.id) +session = response.parse() # get the object that `sessions.create()` would have returned +print(session.id) ``` These methods return an [`APIResponse`](https://github.com/browserbase/sdk-python/tree/main/src/browserbase/_response.py) object. @@ -233,8 +235,9 @@ 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.contexts.with_streaming_response.create( - project_id="projectId", +with client.sessions.with_streaming_response.create( + project_id="your_project_id", + proxies=True, ) as response: print(response.headers.get("X-My-Header")) diff --git a/SECURITY.md b/SECURITY.md index 06c5b6e8..4fdede87 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -20,7 +20,7 @@ or products provided by Browserbase please follow the respective company's secur ### Browserbase Terms and Policies -Please contact dev-feedback@browserbase.com for any questions or concerns regarding security of our services. +Please contact support@browserbase.com for any questions or concerns regarding security of our services. --- diff --git a/pyproject.toml b/pyproject.toml index cf85fb28..efbaac84 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,11 +1,11 @@ [project] name = "browserbase" version = "0.1.0-alpha.6" -description = "The official Python library for the browserbase API" +description = "The official Python library for the Browserbase API" dynamic = ["readme"] license = "Apache-2.0" authors = [ -{ name = "Browserbase", email = "dev-feedback@browserbase.com" }, +{ name = "Browserbase", email = "support@browserbase.com" }, ] dependencies = [ "httpx>=0.23.0, <1", diff --git a/src/browserbase/__init__.py b/src/browserbase/__init__.py index ea5a5985..4b1d2804 100644 --- a/src/browserbase/__init__.py +++ b/src/browserbase/__init__.py @@ -4,7 +4,6 @@ from ._types import NOT_GIVEN, NoneType, NotGiven, Transport, ProxiesTypes from ._utils import file_from_path from ._client import ( - ENVIRONMENTS, Client, Stream, Timeout, @@ -69,7 +68,6 @@ "AsyncStream", "Browserbase", "AsyncBrowserbase", - "ENVIRONMENTS", "file_from_path", "BaseModel", "DEFAULT_TIMEOUT", diff --git a/src/browserbase/_client.py b/src/browserbase/_client.py index 461f9dae..b3ed32f6 100644 --- a/src/browserbase/_client.py +++ b/src/browserbase/_client.py @@ -3,8 +3,8 @@ from __future__ import annotations import os -from typing import Any, Dict, Union, Mapping, cast -from typing_extensions import Self, Literal, override +from typing import Any, Union, Mapping +from typing_extensions import Self, override import httpx @@ -33,7 +33,6 @@ ) __all__ = [ - "ENVIRONMENTS", "Timeout", "Transport", "ProxiesTypes", @@ -45,12 +44,6 @@ "AsyncClient", ] -ENVIRONMENTS: Dict[str, str] = { - "production": "https://api.browserbase.com", - "development": "https://api.dev.browserbase.com", - "local": "http://api.localhost", -} - class Browserbase(SyncAPIClient): contexts: resources.ContextsResource @@ -63,14 +56,11 @@ class Browserbase(SyncAPIClient): # client options api_key: str - _environment: Literal["production", "development", "local"] | NotGiven - def __init__( self, *, api_key: str | None = None, - environment: Literal["production", "development", "local"] | NotGiven = NOT_GIVEN, - base_url: str | httpx.URL | None | NotGiven = NOT_GIVEN, + base_url: str | httpx.URL | None = None, timeout: Union[float, Timeout, None, NotGiven] = NOT_GIVEN, max_retries: int = DEFAULT_MAX_RETRIES, default_headers: Mapping[str, str] | None = None, @@ -89,7 +79,7 @@ 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 Browserbase client instance. This automatically infers the `api_key` argument from the `BROWSERBASE_API_KEY` environment variable if it is not provided. """ @@ -101,31 +91,10 @@ def __init__( ) self.api_key = api_key - self._environment = environment - - 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 `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: - 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 + if base_url is None: + base_url = os.environ.get("BROWSERBASE_BASE_URL") + if base_url is None: + base_url = f"https://api.browserbase.com" super().__init__( version=__version__, @@ -169,7 +138,6 @@ def copy( self, *, api_key: str | None = None, - environment: Literal["production", "development", "local"] | None = None, base_url: str | httpx.URL | None = None, timeout: float | Timeout | None | NotGiven = NOT_GIVEN, http_client: httpx.Client | None = None, @@ -205,7 +173,6 @@ def copy( 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, @@ -263,14 +230,11 @@ class AsyncBrowserbase(AsyncAPIClient): # client options api_key: str - _environment: Literal["production", "development", "local"] | NotGiven - def __init__( self, *, api_key: str | None = None, - environment: Literal["production", "development", "local"] | NotGiven = NOT_GIVEN, - base_url: str | httpx.URL | None | NotGiven = NOT_GIVEN, + base_url: str | httpx.URL | None = None, timeout: Union[float, Timeout, None, NotGiven] = NOT_GIVEN, max_retries: int = DEFAULT_MAX_RETRIES, default_headers: Mapping[str, str] | None = None, @@ -289,7 +253,7 @@ def __init__( # part of our public interface in the future. _strict_response_validation: bool = False, ) -> None: - """Construct a new async browserbase client instance. + """Construct a new async Browserbase client instance. This automatically infers the `api_key` argument from the `BROWSERBASE_API_KEY` environment variable if it is not provided. """ @@ -301,31 +265,10 @@ def __init__( ) self.api_key = api_key - self._environment = environment - - 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 `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: - 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 + if base_url is None: + base_url = os.environ.get("BROWSERBASE_BASE_URL") + if base_url is None: + base_url = f"https://api.browserbase.com" super().__init__( version=__version__, @@ -369,7 +312,6 @@ def copy( self, *, api_key: str | None = None, - environment: Literal["production", "development", "local"] | None = None, base_url: str | httpx.URL | None = None, timeout: float | Timeout | None | NotGiven = NOT_GIVEN, http_client: httpx.AsyncClient | None = None, @@ -405,7 +347,6 @@ def copy( 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, diff --git a/tests/test_client.py b/tests/test_client.py index 9fe3b908..9679e176 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -556,16 +556,6 @@ def test_base_url_env(self) -> None: 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(BROWSERBASE_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") - - client = Browserbase( - base_url=None, api_key=api_key, _strict_response_validation=True, environment="production" - ) - assert str(client.base_url).startswith("https://api.browserbase.com") - @pytest.mark.parametrize( "client", [ @@ -728,12 +718,12 @@ def test_parse_retry_after_header(self, remaining_retries: int, retry_after: str @mock.patch("browserbase._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) -> None: - respx_mock.post("/v1/contexts").mock(side_effect=httpx.TimeoutException("Test timeout error")) + respx_mock.post("/v1/sessions").mock(side_effect=httpx.TimeoutException("Test timeout error")) with pytest.raises(APITimeoutError): self.client.post( - "/v1/contexts", - body=cast(object, dict(project_id="projectId")), + "/v1/sessions", + body=cast(object, dict(project_id="your_project_id", proxies=True)), cast_to=httpx.Response, options={"headers": {RAW_RESPONSE_HEADER: "stream"}}, ) @@ -743,12 +733,12 @@ def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter) -> No @mock.patch("browserbase._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) -> None: - respx_mock.post("/v1/contexts").mock(return_value=httpx.Response(500)) + respx_mock.post("/v1/sessions").mock(return_value=httpx.Response(500)) with pytest.raises(APIStatusError): self.client.post( - "/v1/contexts", - body=cast(object, dict(project_id="projectId")), + "/v1/sessions", + body=cast(object, dict(project_id="your_project_id", proxies=True)), cast_to=httpx.Response, options={"headers": {RAW_RESPONSE_HEADER: "stream"}}, ) @@ -779,9 +769,9 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: return httpx.Response(500) return httpx.Response(200) - respx_mock.post("/v1/contexts").mock(side_effect=retry_handler) + respx_mock.post("/v1/sessions").mock(side_effect=retry_handler) - response = client.contexts.with_raw_response.create(project_id="projectId") + response = client.sessions.with_raw_response.create(project_id="projectId") assert response.retries_taken == failures_before_success assert int(response.http_request.headers.get("x-stainless-retry-count")) == failures_before_success @@ -803,9 +793,9 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: return httpx.Response(500) return httpx.Response(200) - respx_mock.post("/v1/contexts").mock(side_effect=retry_handler) + respx_mock.post("/v1/sessions").mock(side_effect=retry_handler) - response = client.contexts.with_raw_response.create( + response = client.sessions.with_raw_response.create( project_id="projectId", extra_headers={"x-stainless-retry-count": Omit()} ) @@ -828,9 +818,9 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: return httpx.Response(500) return httpx.Response(200) - respx_mock.post("/v1/contexts").mock(side_effect=retry_handler) + respx_mock.post("/v1/sessions").mock(side_effect=retry_handler) - response = client.contexts.with_raw_response.create( + response = client.sessions.with_raw_response.create( project_id="projectId", extra_headers={"x-stainless-retry-count": "42"} ) @@ -1342,16 +1332,6 @@ def test_base_url_env(self) -> None: 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(BROWSERBASE_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") - - client = AsyncBrowserbase( - base_url=None, api_key=api_key, _strict_response_validation=True, environment="production" - ) - assert str(client.base_url).startswith("https://api.browserbase.com") - @pytest.mark.parametrize( "client", [ @@ -1518,12 +1498,12 @@ async def test_parse_retry_after_header(self, remaining_retries: int, retry_afte @mock.patch("browserbase._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) -> None: - respx_mock.post("/v1/contexts").mock(side_effect=httpx.TimeoutException("Test timeout error")) + respx_mock.post("/v1/sessions").mock(side_effect=httpx.TimeoutException("Test timeout error")) with pytest.raises(APITimeoutError): await self.client.post( - "/v1/contexts", - body=cast(object, dict(project_id="projectId")), + "/v1/sessions", + body=cast(object, dict(project_id="your_project_id", proxies=True)), cast_to=httpx.Response, options={"headers": {RAW_RESPONSE_HEADER: "stream"}}, ) @@ -1533,12 +1513,12 @@ async def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter) @mock.patch("browserbase._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) -> None: - respx_mock.post("/v1/contexts").mock(return_value=httpx.Response(500)) + respx_mock.post("/v1/sessions").mock(return_value=httpx.Response(500)) with pytest.raises(APIStatusError): await self.client.post( - "/v1/contexts", - body=cast(object, dict(project_id="projectId")), + "/v1/sessions", + body=cast(object, dict(project_id="your_project_id", proxies=True)), cast_to=httpx.Response, options={"headers": {RAW_RESPONSE_HEADER: "stream"}}, ) @@ -1570,9 +1550,9 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: return httpx.Response(500) return httpx.Response(200) - respx_mock.post("/v1/contexts").mock(side_effect=retry_handler) + respx_mock.post("/v1/sessions").mock(side_effect=retry_handler) - response = await client.contexts.with_raw_response.create(project_id="projectId") + response = await client.sessions.with_raw_response.create(project_id="projectId") assert response.retries_taken == failures_before_success assert int(response.http_request.headers.get("x-stainless-retry-count")) == failures_before_success @@ -1595,9 +1575,9 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: return httpx.Response(500) return httpx.Response(200) - respx_mock.post("/v1/contexts").mock(side_effect=retry_handler) + respx_mock.post("/v1/sessions").mock(side_effect=retry_handler) - response = await client.contexts.with_raw_response.create( + response = await client.sessions.with_raw_response.create( project_id="projectId", extra_headers={"x-stainless-retry-count": Omit()} ) @@ -1621,9 +1601,9 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: return httpx.Response(500) return httpx.Response(200) - respx_mock.post("/v1/contexts").mock(side_effect=retry_handler) + respx_mock.post("/v1/sessions").mock(side_effect=retry_handler) - response = await client.contexts.with_raw_response.create( + response = await client.sessions.with_raw_response.create( project_id="projectId", extra_headers={"x-stainless-retry-count": "42"} ) From 6e18765771598e5f4d7d83be29663a60dce66845 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 28 Oct 2024 23:31:10 +0000 Subject: [PATCH 026/330] chore(internal): version bump (#38) --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/browserbase/_version.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 4f9005ea..b5db7ce1 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.1.0-alpha.6" + ".": "0.1.0-alpha.7" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index efbaac84..75e5ce6c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "browserbase" -version = "0.1.0-alpha.6" +version = "0.1.0-alpha.7" description = "The official Python library for the Browserbase API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/browserbase/_version.py b/src/browserbase/_version.py index 47f88c6c..382ba40d 100644 --- a/src/browserbase/_version.py +++ b/src/browserbase/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "browserbase" -__version__ = "0.1.0-alpha.6" # x-release-please-version +__version__ = "0.1.0-alpha.7" # x-release-please-version From e505b8c525e2025d51864929a38bd9737979233a Mon Sep 17 00:00:00 2001 From: stainless-bot Date: Tue, 29 Oct 2024 01:04:07 +0000 Subject: [PATCH 027/330] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index e5f4ae33..26fe2738 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,2 +1,2 @@ configured_endpoints: 18 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fbrowserbase-9f93c744538f57747ea1385817e21b40c318b65ebc155dca8950268beb280bc9.yml +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fbrowserbase-873c7106986f864ce293afcccbe32239bc102bb7c1d27acfeafaca3b3e819ee3.yml From 4757efc8730e55ff65c35a7b6f4501e55f8a20cb Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 29 Oct 2024 04:43:09 +0000 Subject: [PATCH 028/330] feat(api): api update (#39) --- .stats.yml | 2 +- .../resources/sessions/sessions.py | 5 +- .../types/session_create_params.py | 63 ++++++++++++++++++- tests/api_resources/test_sessions.py | 4 +- 4 files changed, 66 insertions(+), 8 deletions(-) diff --git a/.stats.yml b/.stats.yml index 26fe2738..1a6b2b54 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,2 +1,2 @@ configured_endpoints: 18 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fbrowserbase-873c7106986f864ce293afcccbe32239bc102bb7c1d27acfeafaca3b3e819ee3.yml +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fbrowserbase-b341dd9d5bb77c4f217b94b186763e730fd798fbb773a5e90bb4e2a8d4a2c822.yml diff --git a/src/browserbase/resources/sessions/sessions.py b/src/browserbase/resources/sessions/sessions.py index d298c011..e05a0de3 100644 --- a/src/browserbase/resources/sessions/sessions.py +++ b/src/browserbase/resources/sessions/sessions.py @@ -2,6 +2,7 @@ from __future__ import annotations +from typing import Union, Iterable from typing_extensions import Literal import httpx @@ -104,7 +105,7 @@ def create( browser_settings: session_create_params.BrowserSettings | NotGiven = NOT_GIVEN, extension_id: str | NotGiven = NOT_GIVEN, keep_alive: bool | NotGiven = NOT_GIVEN, - proxies: object | NotGiven = NOT_GIVEN, + proxies: Union[bool, Iterable[session_create_params.ProxiesUnionMember1]] | NotGiven = NOT_GIVEN, region: Literal["us-west-2", "us-east-1", "eu-central-1", "ap-southeast-1"] | NotGiven = NOT_GIVEN, api_timeout: int | NotGiven = NOT_GIVEN, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. @@ -358,7 +359,7 @@ async def create( browser_settings: session_create_params.BrowserSettings | NotGiven = NOT_GIVEN, extension_id: str | NotGiven = NOT_GIVEN, keep_alive: bool | NotGiven = NOT_GIVEN, - proxies: object | NotGiven = NOT_GIVEN, + proxies: Union[bool, Iterable[session_create_params.ProxiesUnionMember1]] | NotGiven = NOT_GIVEN, region: Literal["us-west-2", "us-east-1", "eu-central-1", "ap-southeast-1"] | NotGiven = NOT_GIVEN, api_timeout: int | NotGiven = NOT_GIVEN, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. diff --git a/src/browserbase/types/session_create_params.py b/src/browserbase/types/session_create_params.py index 3b1920a8..bd643b38 100644 --- a/src/browserbase/types/session_create_params.py +++ b/src/browserbase/types/session_create_params.py @@ -2,8 +2,8 @@ from __future__ import annotations -from typing import List -from typing_extensions import Literal, Required, Annotated, TypedDict +from typing import List, Union, Iterable +from typing_extensions import Literal, Required, Annotated, TypeAlias, TypedDict from .._utils import PropertyInfo @@ -14,6 +14,10 @@ "BrowserSettingsFingerprint", "BrowserSettingsFingerprintScreen", "BrowserSettingsViewport", + "ProxiesUnionMember1", + "ProxiesUnionMember1BrowserbaseProxyConfig", + "ProxiesUnionMember1BrowserbaseProxyConfigGeolocation", + "ProxiesUnionMember1ExternalProxyConfig", ] @@ -38,7 +42,7 @@ class SessionCreateParams(TypedDict, total=False): This is available on the Startup plan only. """ - proxies: object + proxies: Union[bool, Iterable[ProxiesUnionMember1]] """Proxy configuration. Can be true for default proxy, or an array of proxy configurations. @@ -130,3 +134,56 @@ class BrowserSettings(TypedDict, total=False): """Enable or disable captcha solving in the browser. Defaults to `true`.""" viewport: BrowserSettingsViewport + + +class ProxiesUnionMember1BrowserbaseProxyConfigGeolocation(TypedDict, total=False): + country: Required[str] + """Country code in ISO 3166-1 alpha-2 format""" + + city: str + """Name of the city. Use spaces for multi-word city names. Optional.""" + + state: str + """US state code (2 characters). Must also specify US as the country. Optional.""" + + +class ProxiesUnionMember1BrowserbaseProxyConfig(TypedDict, total=False): + type: Required[Literal["browserbase"]] + """Type of proxy. + + Always use 'browserbase' for the Browserbase managed proxy network. + """ + + domain_pattern: Annotated[str, PropertyInfo(alias="domainPattern")] + """Domain pattern for which this proxy should be used. + + If omitted, defaults to all domains. Optional. + """ + + geolocation: ProxiesUnionMember1BrowserbaseProxyConfigGeolocation + """Configuration for geolocation""" + + +class ProxiesUnionMember1ExternalProxyConfig(TypedDict, total=False): + server: Required[str] + """Server URL for external proxy. Required.""" + + type: Required[Literal["external"]] + """Type of proxy. Always 'external' for this config.""" + + domain_pattern: Annotated[str, PropertyInfo(alias="domainPattern")] + """Domain pattern for which this proxy should be used. + + If omitted, defaults to all domains. Optional. + """ + + password: str + """Password for external proxy authentication. Optional.""" + + username: str + """Username for external proxy authentication. Optional.""" + + +ProxiesUnionMember1: TypeAlias = Union[ + ProxiesUnionMember1BrowserbaseProxyConfig, ProxiesUnionMember1ExternalProxyConfig +] diff --git a/tests/api_resources/test_sessions.py b/tests/api_resources/test_sessions.py index dadadd46..8ebd5daf 100644 --- a/tests/api_resources/test_sessions.py +++ b/tests/api_resources/test_sessions.py @@ -63,7 +63,7 @@ def test_method_create_with_all_params(self, client: Browserbase) -> None: }, extension_id="extensionId", keep_alive=True, - proxies={}, + proxies=True, region="us-west-2", api_timeout=60, ) @@ -292,7 +292,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncBrowserbas }, extension_id="extensionId", keep_alive=True, - proxies={}, + proxies=True, region="us-west-2", api_timeout=60, ) From 3795ab449fb57318d8d0b9fffa250713f166a3cd Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 29 Oct 2024 05:10:35 +0000 Subject: [PATCH 029/330] feat(api): api update (#41) --- README.md | 7 ------- tests/test_client.py | 8 ++++---- 2 files changed, 4 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 53e20a98..5d2ebc22 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,6 @@ client = Browserbase( session = client.sessions.create( project_id="your_project_id", - proxies=True, ) print(session.id) ``` @@ -65,7 +64,6 @@ client = AsyncBrowserbase( async def main() -> None: session = await client.sessions.create( project_id="your_project_id", - proxies=True, ) print(session.id) @@ -102,7 +100,6 @@ client = Browserbase() try: client.sessions.create( project_id="your_project_id", - proxies=True, ) except browserbase.APIConnectionError as e: print("The server could not be reached") @@ -148,7 +145,6 @@ client = Browserbase( # Or, configure per-request: client.with_options(max_retries=5).sessions.create( project_id="your_project_id", - proxies=True, ) ``` @@ -174,7 +170,6 @@ client = Browserbase( # Override per-request: client.with_options(timeout=5.0).sessions.create( project_id="your_project_id", - proxies=True, ) ``` @@ -216,7 +211,6 @@ from browserbase import Browserbase client = Browserbase() response = client.sessions.with_raw_response.create( project_id="your_project_id", - proxies=True, ) print(response.headers.get('X-My-Header')) @@ -237,7 +231,6 @@ To stream the response body, use `.with_streaming_response` instead, which requi ```python with client.sessions.with_streaming_response.create( project_id="your_project_id", - proxies=True, ) as response: print(response.headers.get("X-My-Header")) diff --git a/tests/test_client.py b/tests/test_client.py index 9679e176..c70ef50e 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -723,7 +723,7 @@ def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter) -> No with pytest.raises(APITimeoutError): self.client.post( "/v1/sessions", - body=cast(object, dict(project_id="your_project_id", proxies=True)), + body=cast(object, dict(project_id="your_project_id")), cast_to=httpx.Response, options={"headers": {RAW_RESPONSE_HEADER: "stream"}}, ) @@ -738,7 +738,7 @@ def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter) -> Non with pytest.raises(APIStatusError): self.client.post( "/v1/sessions", - body=cast(object, dict(project_id="your_project_id", proxies=True)), + body=cast(object, dict(project_id="your_project_id")), cast_to=httpx.Response, options={"headers": {RAW_RESPONSE_HEADER: "stream"}}, ) @@ -1503,7 +1503,7 @@ async def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter) with pytest.raises(APITimeoutError): await self.client.post( "/v1/sessions", - body=cast(object, dict(project_id="your_project_id", proxies=True)), + body=cast(object, dict(project_id="your_project_id")), cast_to=httpx.Response, options={"headers": {RAW_RESPONSE_HEADER: "stream"}}, ) @@ -1518,7 +1518,7 @@ async def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter) with pytest.raises(APIStatusError): await self.client.post( "/v1/sessions", - body=cast(object, dict(project_id="your_project_id", proxies=True)), + body=cast(object, dict(project_id="your_project_id")), cast_to=httpx.Response, options={"headers": {RAW_RESPONSE_HEADER: "stream"}}, ) From 743da6da2b3c00330547410bfb4d20e0905ded0c Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 29 Oct 2024 05:32:37 +0000 Subject: [PATCH 030/330] chore(internal): version bump (#43) --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/browserbase/_version.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index b5db7ce1..f0b371d4 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.1.0-alpha.7" + ".": "1.0.0-alpha.0" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 75e5ce6c..8e378fe2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "browserbase" -version = "0.1.0-alpha.7" +version = "1.0.0-alpha.0" description = "The official Python library for the Browserbase API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/browserbase/_version.py b/src/browserbase/_version.py index 382ba40d..596462f5 100644 --- a/src/browserbase/_version.py +++ b/src/browserbase/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "browserbase" -__version__ = "0.1.0-alpha.7" # x-release-please-version +__version__ = "1.0.0-alpha.0" # x-release-please-version From c9121c2f49b19bc8b3201ceaa698b275112ea63a Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 29 Oct 2024 05:55:31 +0000 Subject: [PATCH 031/330] feat(api): api update (#44) --- README.md | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 5d2ebc22..91202218 100644 --- a/README.md +++ b/README.md @@ -15,13 +15,10 @@ The REST API documentation can be found on [docs.browserbase.com](https://docs.b ## Installation ```sh -# install from the production repo -pip install git+ssh://git@github.com/browserbase/sdk-python.git +# install from PyPI +pip install --pre browserbase ``` -> [!NOTE] -> Once this package is [published to PyPI](https://app.stainlessapi.com/docs/guides/publish), this will become: `pip install --pre browserbase` - ## Usage The full API of this library can be found in [api.md](api.md). From 8818717fc2be0bbc04d083a32283df3cb626549f Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 29 Oct 2024 06:02:32 +0000 Subject: [PATCH 032/330] chore(internal): version bump (#46) --- .release-please-manifest.json | 2 +- README.md | 2 +- pyproject.toml | 2 +- src/browserbase/_version.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index f0b371d4..fea34540 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "1.0.0-alpha.0" + ".": "1.0.0" } \ No newline at end of file diff --git a/README.md b/README.md index 91202218..e6c65d1c 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ The REST API documentation can be found on [docs.browserbase.com](https://docs.b ```sh # install from PyPI -pip install --pre browserbase +pip install browserbase ``` ## Usage diff --git a/pyproject.toml b/pyproject.toml index 8e378fe2..1e25e697 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "browserbase" -version = "1.0.0-alpha.0" +version = "1.0.0" description = "The official Python library for the Browserbase API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/browserbase/_version.py b/src/browserbase/_version.py index 596462f5..1f27c648 100644 --- a/src/browserbase/_version.py +++ b/src/browserbase/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "browserbase" -__version__ = "1.0.0-alpha.0" # x-release-please-version +__version__ = "1.0.0" # x-release-please-version From a27e4dd755857f4eecb95d0471ddbd20791ce0c7 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 31 Oct 2024 22:18:20 +0000 Subject: [PATCH 033/330] feat(api): api update (#48) --- .stats.yml | 2 +- src/browserbase/resources/projects.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.stats.yml b/.stats.yml index 1a6b2b54..b04bed28 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,2 +1,2 @@ configured_endpoints: 18 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fbrowserbase-b341dd9d5bb77c4f217b94b186763e730fd798fbb773a5e90bb4e2a8d4a2c822.yml +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fbrowserbase-d8e42f141c0955e8100ca3ce041ce8dedf5dcf68b04e554a5704e4c2003c2fd4.yml diff --git a/src/browserbase/resources/projects.py b/src/browserbase/resources/projects.py index f8b1936a..bf4a5df9 100644 --- a/src/browserbase/resources/projects.py +++ b/src/browserbase/resources/projects.py @@ -84,7 +84,7 @@ def list( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> ProjectListResponse: - """List all projects""" + """List projects""" return self._get( "/v1/projects", options=make_request_options( @@ -190,7 +190,7 @@ async def list( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> ProjectListResponse: - """List all projects""" + """List projects""" return await self._get( "/v1/projects", options=make_request_options( From 1e5091b50a936930545954e56f432707ec4e1d08 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 5 Nov 2024 23:36:53 +0000 Subject: [PATCH 034/330] feat(api): api update (#51) --- .stats.yml | 2 +- README.md | 4 ++-- pyproject.toml | 5 ++--- requirements-dev.lock | 2 +- src/browserbase/_compat.py | 6 ++++-- src/browserbase/_models.py | 9 +++++--- src/browserbase/_utils/__init__.py | 1 + src/browserbase/_utils/_transform.py | 9 ++++++-- src/browserbase/_utils/_utils.py | 17 +++++++++++++++ .../types/sessions/session_recording.py | 2 -- tests/test_models.py | 21 +++++++------------ tests/test_transform.py | 15 +++++++++++++ 12 files changed, 63 insertions(+), 30 deletions(-) diff --git a/.stats.yml b/.stats.yml index b04bed28..d42b050b 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,2 +1,2 @@ configured_endpoints: 18 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fbrowserbase-d8e42f141c0955e8100ca3ce041ce8dedf5dcf68b04e554a5704e4c2003c2fd4.yml +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fbrowserbase-7f88912695bab2b98cb73137e6f36125d02fdfaf8eed4532ee1c82385609a259.yml diff --git a/README.md b/README.md index e6c65d1c..8741d166 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![PyPI version](https://img.shields.io/pypi/v/browserbase.svg)](https://pypi.org/project/browserbase/) -The Browserbase Python library provides convenient access to the Browserbase REST API from any Python 3.7+ +The Browserbase Python library provides convenient access to the Browserbase REST API from any Python 3.8+ 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). @@ -328,7 +328,7 @@ print(browserbase.__version__) ## Requirements -Python 3.7 or higher. +Python 3.8 or higher. ## Contributing diff --git a/pyproject.toml b/pyproject.toml index 1e25e697..85ea7be9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,11 +16,10 @@ dependencies = [ "sniffio", "cached-property; python_version < '3.8'", ] -requires-python = ">= 3.7" +requires-python = ">= 3.8" classifiers = [ "Typing :: Typed", "Intended Audience :: Developers", - "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", @@ -139,7 +138,7 @@ filterwarnings = [ # 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.7" +pythonVersion = "3.8" exclude = [ "_dev", diff --git a/requirements-dev.lock b/requirements-dev.lock index 20a4ebdc..ae83305c 100644 --- a/requirements-dev.lock +++ b/requirements-dev.lock @@ -48,7 +48,7 @@ markdown-it-py==3.0.0 # via rich mdurl==0.1.2 # via markdown-it-py -mypy==1.11.2 +mypy==1.13.0 mypy-extensions==1.0.0 # via mypy nodeenv==1.8.0 diff --git a/src/browserbase/_compat.py b/src/browserbase/_compat.py index d89920d9..4794129c 100644 --- a/src/browserbase/_compat.py +++ b/src/browserbase/_compat.py @@ -2,7 +2,7 @@ from typing import TYPE_CHECKING, Any, Union, Generic, TypeVar, Callable, cast, overload from datetime import date, datetime -from typing_extensions import Self +from typing_extensions import Self, Literal import pydantic from pydantic.fields import FieldInfo @@ -137,9 +137,11 @@ def model_dump( exclude_unset: bool = False, exclude_defaults: bool = False, warnings: bool = True, + mode: Literal["json", "python"] = "python", ) -> dict[str, Any]: - if PYDANTIC_V2: + if PYDANTIC_V2 or hasattr(model, "model_dump"): return model.model_dump( + mode=mode, exclude=exclude, exclude_unset=exclude_unset, exclude_defaults=exclude_defaults, diff --git a/src/browserbase/_models.py b/src/browserbase/_models.py index 42551b76..6cb469e2 100644 --- a/src/browserbase/_models.py +++ b/src/browserbase/_models.py @@ -37,6 +37,7 @@ PropertyInfo, is_list, is_given, + json_safe, lru_cache, is_mapping, parse_date, @@ -279,8 +280,8 @@ def model_dump( Returns: A dictionary representation of the model. """ - if mode != "python": - raise ValueError("mode is only supported in Pydantic v2") + 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: @@ -289,7 +290,7 @@ def model_dump( 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") - return super().dict( # pyright: ignore[reportDeprecated] + dumped = super().dict( # pyright: ignore[reportDeprecated] include=include, exclude=exclude, by_alias=by_alias, @@ -298,6 +299,8 @@ def model_dump( exclude_none=exclude_none, ) + return cast(dict[str, Any], json_safe(dumped)) if mode == "json" else dumped + @override def model_dump_json( self, diff --git a/src/browserbase/_utils/__init__.py b/src/browserbase/_utils/__init__.py index 3efe66c8..a7cff3c0 100644 --- a/src/browserbase/_utils/__init__.py +++ b/src/browserbase/_utils/__init__.py @@ -6,6 +6,7 @@ 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, diff --git a/src/browserbase/_utils/_transform.py b/src/browserbase/_utils/_transform.py index 47e262a5..d7c05345 100644 --- a/src/browserbase/_utils/_transform.py +++ b/src/browserbase/_utils/_transform.py @@ -173,6 +173,11 @@ def _transform_recursive( # Iterable[T] or (is_iterable_type(stripped_type) and is_iterable(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) return [_transform_recursive(d, annotation=annotation, inner_type=inner_type) for d in data] @@ -186,7 +191,7 @@ def _transform_recursive( return data if isinstance(data, pydantic.BaseModel): - return model_dump(data, exclude_unset=True) + return model_dump(data, exclude_unset=True, mode="json") annotated_type = _get_annotated_type(annotation) if annotated_type is None: @@ -324,7 +329,7 @@ async def _async_transform_recursive( return data if isinstance(data, pydantic.BaseModel): - return model_dump(data, exclude_unset=True) + return model_dump(data, exclude_unset=True, mode="json") annotated_type = _get_annotated_type(annotation) if annotated_type is None: diff --git a/src/browserbase/_utils/_utils.py b/src/browserbase/_utils/_utils.py index 0bba17ca..e5811bba 100644 --- a/src/browserbase/_utils/_utils.py +++ b/src/browserbase/_utils/_utils.py @@ -16,6 +16,7 @@ overload, ) from pathlib import Path +from datetime import date, datetime from typing_extensions import TypeGuard import sniffio @@ -395,3 +396,19 @@ def lru_cache(*, maxsize: int | None = 128) -> Callable[[CallableT], CallableT]: 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/browserbase/types/sessions/session_recording.py b/src/browserbase/types/sessions/session_recording.py index d3e0325a..c8471371 100644 --- a/src/browserbase/types/sessions/session_recording.py +++ b/src/browserbase/types/sessions/session_recording.py @@ -10,8 +10,6 @@ class SessionRecording(BaseModel): - id: str - data: Dict[str, object] """ See diff --git a/tests/test_models.py b/tests/test_models.py index 5b8044f0..c199e942 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -520,19 +520,15 @@ class Model(BaseModel): assert m3.to_dict(exclude_none=True) == {} assert m3.to_dict(exclude_defaults=True) == {} - if PYDANTIC_V2: - - class Model2(BaseModel): - created_at: datetime + 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} - else: - with pytest.raises(ValueError, match="mode is only supported in Pydantic v2"): - m.to_dict(mode="json") + 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 not PYDANTIC_V2: with pytest.raises(ValueError, match="warnings is only supported in Pydantic v2"): m.to_dict(warnings=False) @@ -558,9 +554,6 @@ class Model(BaseModel): assert m3.model_dump(exclude_none=True) == {} if not PYDANTIC_V2: - with pytest.raises(ValueError, match="mode is only supported in Pydantic v2"): - m.model_dump(mode="json") - with pytest.raises(ValueError, match="round_trip is only supported in Pydantic v2"): m.model_dump(round_trip=True) diff --git a/tests/test_transform.py b/tests/test_transform.py index 436b8185..03c2ecd4 100644 --- a/tests/test_transform.py +++ b/tests/test_transform.py @@ -177,17 +177,32 @@ 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 = "Z" if PYDANTIC_V2 else "+00:00" 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 From 869e144c549fe5769e58913cda654f89880c3942 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 8 Nov 2024 18:48:16 +0000 Subject: [PATCH 035/330] chore: rebuild project due to codegen change (#53) --- tests/test_client.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_client.py b/tests/test_client.py index c70ef50e..9cf62b33 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -703,7 +703,7 @@ class Model(BaseModel): [3, "", 0.5], [2, "", 0.5 * 2.0], [1, "", 0.5 * 4.0], - [-1100, "", 7.8], # test large number potentially overflowing + [-1100, "", 8], # test large number potentially overflowing ], ) @mock.patch("time.time", mock.MagicMock(return_value=1696004797)) @@ -1482,7 +1482,7 @@ class Model(BaseModel): [3, "", 0.5], [2, "", 0.5 * 2.0], [1, "", 0.5 * 4.0], - [-1100, "", 7.8], # test large number potentially overflowing + [-1100, "", 8], # test large number potentially overflowing ], ) @mock.patch("time.time", mock.MagicMock(return_value=1696004797)) From 97b52fd27cce8e166d02278d963ec3ec42685803 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 11 Nov 2024 11:10:26 +0000 Subject: [PATCH 036/330] chore: rebuild project due to codegen change (#54) --- src/browserbase/resources/sessions/sessions.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/browserbase/resources/sessions/sessions.py b/src/browserbase/resources/sessions/sessions.py index e05a0de3..15d8e700 100644 --- a/src/browserbase/resources/sessions/sessions.py +++ b/src/browserbase/resources/sessions/sessions.py @@ -155,7 +155,7 @@ def create( "keep_alive": keep_alive, "proxies": proxies, "region": region, - "timeout": api_timeout, + "api_timeout": api_timeout, }, session_create_params.SessionCreateParams, ), @@ -409,7 +409,7 @@ async def create( "keep_alive": keep_alive, "proxies": proxies, "region": region, - "timeout": api_timeout, + "api_timeout": api_timeout, }, session_create_params.SessionCreateParams, ), From caa137bbf817dfd838ea740a98c779f22dcba785 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 11 Nov 2024 22:23:41 +0000 Subject: [PATCH 037/330] chore: rebuild project due to codegen change (#55) --- README.md | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 8741d166..52eba200 100644 --- a/README.md +++ b/README.md @@ -28,8 +28,7 @@ import os from browserbase import Browserbase client = Browserbase( - # This is the default and can be omitted - api_key=os.environ.get("BROWSERBASE_API_KEY"), + api_key=os.environ.get("BROWSERBASE_API_KEY"), # This is the default and can be omitted ) session = client.sessions.create( @@ -53,8 +52,7 @@ import asyncio from browserbase import AsyncBrowserbase client = AsyncBrowserbase( - # This is the default and can be omitted - api_key=os.environ.get("BROWSERBASE_API_KEY"), + api_key=os.environ.get("BROWSERBASE_API_KEY"), # This is the default and can be omitted ) From fc66e82ae31e83a6927b5efd399b0ba7ccab80c3 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 12 Nov 2024 12:09:01 +0000 Subject: [PATCH 038/330] chore: rebuild project due to codegen change (#57) --- src/browserbase/_utils/_transform.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/browserbase/_utils/_transform.py b/src/browserbase/_utils/_transform.py index d7c05345..a6b62cad 100644 --- a/src/browserbase/_utils/_transform.py +++ b/src/browserbase/_utils/_transform.py @@ -316,6 +316,11 @@ async def _async_transform_recursive( # Iterable[T] or (is_iterable_type(stripped_type) and is_iterable(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) return [await _async_transform_recursive(d, annotation=annotation, inner_type=inner_type) for d in data] From 76566a314b7c1bd28aae9ba264e48b49cac96a50 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 18 Nov 2024 10:26:51 +0000 Subject: [PATCH 039/330] chore: rebuild project due to codegen change (#58) --- tests/api_resources/test_sessions.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/api_resources/test_sessions.py b/tests/api_resources/test_sessions.py index 8ebd5daf..7b9fbce1 100644 --- a/tests/api_resources/test_sessions.py +++ b/tests/api_resources/test_sessions.py @@ -41,11 +41,11 @@ def test_method_create_with_all_params(self, client: Browserbase) -> None: }, "extension_id": "extensionId", "fingerprint": { - "browsers": ["chrome", "edge", "firefox"], - "devices": ["desktop", "mobile"], + "browsers": ["chrome"], + "devices": ["desktop"], "http_version": 1, - "locales": ["string", "string", "string"], - "operating_systems": ["android", "ios", "linux"], + "locales": ["string"], + "operating_systems": ["android"], "screen": { "max_height": 0, "max_width": 0, @@ -270,11 +270,11 @@ async def test_method_create_with_all_params(self, async_client: AsyncBrowserbas }, "extension_id": "extensionId", "fingerprint": { - "browsers": ["chrome", "edge", "firefox"], - "devices": ["desktop", "mobile"], + "browsers": ["chrome"], + "devices": ["desktop"], "http_version": 1, - "locales": ["string", "string", "string"], - "operating_systems": ["android", "ios", "linux"], + "locales": ["string"], + "operating_systems": ["android"], "screen": { "max_height": 0, "max_width": 0, From 719a99be6b824a2a44d024eccc109bc0af91ff47 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 18 Nov 2024 12:46:17 +0000 Subject: [PATCH 040/330] chore: rebuild project due to codegen change (#59) --- pyproject.toml | 1 + requirements-dev.lock | 1 + src/browserbase/_utils/_sync.py | 90 +++++++++++++++------------------ tests/test_client.py | 38 ++++++++++++++ 4 files changed, 80 insertions(+), 50 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 85ea7be9..23b6054f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -55,6 +55,7 @@ dev-dependencies = [ "dirty-equals>=0.6.0", "importlib-metadata>=6.7.0", "rich>=13.7.1", + "nest_asyncio==1.6.0" ] [tool.rye.scripts] diff --git a/requirements-dev.lock b/requirements-dev.lock index ae83305c..968b92ed 100644 --- a/requirements-dev.lock +++ b/requirements-dev.lock @@ -51,6 +51,7 @@ mdurl==0.1.2 mypy==1.13.0 mypy-extensions==1.0.0 # via mypy +nest-asyncio==1.6.0 nodeenv==1.8.0 # via pyright nox==2023.4.22 diff --git a/src/browserbase/_utils/_sync.py b/src/browserbase/_utils/_sync.py index d0d81033..8b3aaf2b 100644 --- a/src/browserbase/_utils/_sync.py +++ b/src/browserbase/_utils/_sync.py @@ -1,56 +1,62 @@ from __future__ import annotations +import sys +import asyncio import functools -from typing import TypeVar, Callable, Awaitable +import contextvars +from typing import Any, TypeVar, Callable, Awaitable from typing_extensions import ParamSpec -import anyio -import anyio.to_thread - -from ._reflection import function_has_argument - T_Retval = TypeVar("T_Retval") T_ParamSpec = ParamSpec("T_ParamSpec") -# copied from `asyncer`, https://github.com/tiangolo/asyncer -def asyncify( - function: Callable[T_ParamSpec, T_Retval], - *, - cancellable: bool = False, - limiter: anyio.CapacityLimiter | None = None, -) -> Callable[T_ParamSpec, Awaitable[T_Retval]]: +if sys.version_info >= (3, 9): + to_thread = asyncio.to_thread +else: + # backport of https://docs.python.org/3/library/asyncio-task.html#asyncio.to_thread + # for Python 3.8 support + async def to_thread( + func: Callable[T_ParamSpec, T_Retval], /, *args: T_ParamSpec.args, **kwargs: T_ParamSpec.kwargs + ) -> Any: + """Asynchronously run function *func* in a separate thread. + + Any *args and **kwargs supplied for this function are directly passed + to *func*. Also, the current :class:`contextvars.Context` is propagated, + allowing context variables from the main thread to be accessed in the + separate thread. + + Returns a coroutine that can be awaited to get the eventual result of *func*. + """ + loop = asyncio.events.get_running_loop() + ctx = contextvars.copy_context() + func_call = functools.partial(ctx.run, func, *args, **kwargs) + return await loop.run_in_executor(None, func_call) + + +# 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, and that when called, calls the original function - in a worker thread using `anyio.to_thread.run_sync()`. Internally, - `asyncer.asyncify()` uses the same `anyio.to_thread.run_sync()`, but it supports - keyword arguments additional to positional arguments and it adds better support for - autocompletion and inline errors for the arguments of the function called and the - return value. - - If the `cancellable` option is enabled and the task waiting for its completion is - cancelled, the thread will still run its course but its return value (or any raised - exception) will be ignored. + positional and keyword arguments. For python version 3.9 and above, it uses + asyncio.to_thread to run the function in a separate thread. For python version + 3.8, it uses locally defined copy of the asyncio.to_thread function which was + introduced in python 3.9. - Use it like this: + Usage: - ```Python - def do_work(arg1, arg2, kwarg1="", kwarg2="") -> str: - # Do work - return "Some result" + ```python + def blocking_func(arg1, arg2, kwarg1=None): + # blocking code + return result - result = await to_thread.asyncify(do_work)("spam", "ham", kwarg1="a", kwarg2="b") - print(result) + result = asyncify(blocking_function)(arg1, arg2, kwarg1=value1) ``` ## Arguments `function`: a blocking regular callable (e.g. a function) - `cancellable`: `True` to allow cancellation of the operation - `limiter`: capacity limiter to use to limit the total amount of threads running - (if omitted, the default limiter is used) ## Return @@ -60,22 +66,6 @@ def do_work(arg1, arg2, kwarg1="", kwarg2="") -> str: """ async def wrapper(*args: T_ParamSpec.args, **kwargs: T_ParamSpec.kwargs) -> T_Retval: - partial_f = functools.partial(function, *args, **kwargs) - - # In `v4.1.0` anyio added the `abandon_on_cancel` argument and deprecated the old - # `cancellable` argument, so we need to use the new `abandon_on_cancel` to avoid - # surfacing deprecation warnings. - if function_has_argument(anyio.to_thread.run_sync, "abandon_on_cancel"): - return await anyio.to_thread.run_sync( - partial_f, - abandon_on_cancel=cancellable, - limiter=limiter, - ) - - return await anyio.to_thread.run_sync( - partial_f, - cancellable=cancellable, - limiter=limiter, - ) + return await to_thread(function, *args, **kwargs) return wrapper diff --git a/tests/test_client.py b/tests/test_client.py index 9cf62b33..aa0043e2 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -4,11 +4,14 @@ import gc import os +import sys import json import asyncio import inspect +import subprocess import tracemalloc from typing import Any, Union, cast +from textwrap import dedent from unittest import mock from typing_extensions import Literal @@ -1608,3 +1611,38 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: ) assert response.http_request.headers.get("x-stainless-retry-count") == "42" + + def test_get_platform(self) -> None: + # A previous implementation of asyncify could leave threads unterminated when + # used with nest_asyncio. + # + # Since nest_asyncio.apply() is global and cannot be un-applied, this + # test is run in a separate process to avoid affecting other tests. + test_code = dedent(""" + import asyncio + import nest_asyncio + import threading + + from browserbase._utils import asyncify + from browserbase._base_client import get_platform + + async def test_main() -> None: + result = await asyncify(get_platform)() + print(result) + for thread in threading.enumerate(): + print(thread.name) + + nest_asyncio.apply() + asyncio.run(test_main()) + """) + with subprocess.Popen( + [sys.executable, "-c", test_code], + text=True, + ) as process: + try: + process.wait(2) + if process.returncode: + raise AssertionError("calling get_platform using asyncify resulted in a non-zero exit code") + except subprocess.TimeoutExpired as e: + process.kill() + raise AssertionError("calling get_platform using asyncify resulted in a hung process") from e From 11e85a33af4b8ae0df05e60e3a8172dd687ae4a9 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 19 Nov 2024 16:49:43 +0000 Subject: [PATCH 041/330] chore(internal): version bump (#62) --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/browserbase/_version.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index fea34540..a8f71224 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "1.0.0" + ".": "1.0.1" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 23b6054f..94f46c76 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "browserbase" -version = "1.0.0" +version = "1.0.1" description = "The official Python library for the Browserbase API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/browserbase/_version.py b/src/browserbase/_version.py index 1f27c648..09428425 100644 --- a/src/browserbase/_version.py +++ b/src/browserbase/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "browserbase" -__version__ = "1.0.0" # x-release-please-version +__version__ = "1.0.1" # x-release-please-version From b54d08afc51d10d5d9c80aac3fb85bf02203e987 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 21 Nov 2024 17:34:21 +0000 Subject: [PATCH 042/330] chore(internal): version bump (#64) --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/browserbase/_version.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index a8f71224..06d6df24 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "1.0.1" + ".": "1.0.2" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 94f46c76..c7c36f53 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "browserbase" -version = "1.0.1" +version = "1.0.2" description = "The official Python library for the Browserbase API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/browserbase/_version.py b/src/browserbase/_version.py index 09428425..bf5d1e11 100644 --- a/src/browserbase/_version.py +++ b/src/browserbase/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "browserbase" -__version__ = "1.0.1" # x-release-please-version +__version__ = "1.0.2" # x-release-please-version From fe02429e0b46de28e68b36a348844e679b49d3d4 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 22 Nov 2024 11:28:02 +0000 Subject: [PATCH 043/330] chore(internal): fix compat model_dump method when warnings are passed (#65) --- src/browserbase/_compat.py | 3 ++- tests/test_models.py | 8 ++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/browserbase/_compat.py b/src/browserbase/_compat.py index 4794129c..df173f85 100644 --- a/src/browserbase/_compat.py +++ b/src/browserbase/_compat.py @@ -145,7 +145,8 @@ def model_dump( exclude=exclude, exclude_unset=exclude_unset, exclude_defaults=exclude_defaults, - warnings=warnings, + # warnings are not supported in Pydantic v1 + warnings=warnings if PYDANTIC_V2 else True, ) return cast( "dict[str, Any]", diff --git a/tests/test_models.py b/tests/test_models.py index c199e942..37671b06 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -561,6 +561,14 @@ class Model(BaseModel): 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) From 638dc724587796d39f631001fe61ab18f34caa46 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 22 Nov 2024 18:55:37 +0000 Subject: [PATCH 044/330] chore(internal): version bump (#67) --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/browserbase/_version.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 06d6df24..b7634f9b 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "1.0.2" + ".": "1.0.3" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index c7c36f53..769dc0c2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "browserbase" -version = "1.0.2" +version = "1.0.3" description = "The official Python library for the Browserbase API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/browserbase/_version.py b/src/browserbase/_version.py index bf5d1e11..f1870ed8 100644 --- a/src/browserbase/_version.py +++ b/src/browserbase/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "browserbase" -__version__ = "1.0.2" # x-release-please-version +__version__ = "1.0.3" # x-release-please-version From fb729a76d8f680dc9de43d545beef0c48d4d73f1 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 29 Nov 2024 16:54:31 +0000 Subject: [PATCH 045/330] chore(internal): codegen related update (#68) --- README.md | 6 ++++-- pyproject.toml | 1 - src/browserbase/_compat.py | 5 +---- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 52eba200..d4d7e885 100644 --- a/README.md +++ b/README.md @@ -178,12 +178,14 @@ 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 `debug`. +You can enable logging by setting the environment variable `BROWSERBASE_LOG` to `info`. ```shell -$ export BROWSERBASE_LOG=debug +$ export BROWSERBASE_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`: diff --git a/pyproject.toml b/pyproject.toml index 769dc0c2..af4a0642 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,7 +14,6 @@ dependencies = [ "anyio>=3.5.0, <5", "distro>=1.7.0, <2", "sniffio", - "cached-property; python_version < '3.8'", ] requires-python = ">= 3.8" classifiers = [ diff --git a/src/browserbase/_compat.py b/src/browserbase/_compat.py index df173f85..92d9ee61 100644 --- a/src/browserbase/_compat.py +++ b/src/browserbase/_compat.py @@ -214,9 +214,6 @@ 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: - try: - from functools import cached_property as cached_property - except ImportError: - from cached_property import cached_property as cached_property + from functools import cached_property as cached_property typed_cached_property = cached_property From 297122945d985f5598bf2c9ccd4643b611586444 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 29 Nov 2024 17:03:23 +0000 Subject: [PATCH 046/330] chore(internal): exclude mypy from running on tests (#70) --- mypy.ini | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/mypy.ini b/mypy.ini index 94f60e5e..9e79a7c7 100644 --- a/mypy.ini +++ b/mypy.ini @@ -5,7 +5,10 @@ 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. -exclude = ^(src/browserbase/_files\.py|_dev/.*\.py)$ +# +# We also exclude our `tests` as mypy doesn't always infer +# types correctly and Pyright will still catch any type errors. +exclude = ^(src/browserbase/_files\.py|_dev/.*\.py|tests/.*)$ strict_equality = True implicit_reexport = True From ca60315b8b12c27c22087a6cf8135da6c0df3e53 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 29 Nov 2024 17:15:41 +0000 Subject: [PATCH 047/330] fix(client): compat with new httpx 0.28.0 release (#71) --- src/browserbase/_base_client.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/browserbase/_base_client.py b/src/browserbase/_base_client.py index f17e8d2b..d8b28d9a 100644 --- a/src/browserbase/_base_client.py +++ b/src/browserbase/_base_client.py @@ -792,6 +792,7 @@ def __init__( custom_query: Mapping[str, object] | None = None, _strict_response_validation: bool, ) -> None: + kwargs: dict[str, Any] = {} if limits is not None: warnings.warn( "The `connection_pool_limits` argument is deprecated. The `http_client` argument should be passed instead", @@ -804,6 +805,7 @@ def __init__( limits = DEFAULT_CONNECTION_LIMITS if transport is not None: + kwargs["transport"] = transport warnings.warn( "The `transport` argument is deprecated. The `http_client` argument should be passed instead", category=DeprecationWarning, @@ -813,6 +815,7 @@ def __init__( raise ValueError("The `http_client` argument is mutually exclusive with `transport`") if proxies is not None: + kwargs["proxies"] = proxies warnings.warn( "The `proxies` argument is deprecated. The `http_client` argument should be passed instead", category=DeprecationWarning, @@ -856,10 +859,9 @@ def __init__( base_url=base_url, # cast to a valid type because mypy doesn't understand our type narrowing timeout=cast(Timeout, timeout), - proxies=proxies, - transport=transport, limits=limits, follow_redirects=True, + **kwargs, # type: ignore ) def is_closed(self) -> bool: @@ -1358,6 +1360,7 @@ def __init__( custom_headers: Mapping[str, str] | None = None, custom_query: Mapping[str, object] | None = None, ) -> None: + kwargs: dict[str, Any] = {} if limits is not None: warnings.warn( "The `connection_pool_limits` argument is deprecated. The `http_client` argument should be passed instead", @@ -1370,6 +1373,7 @@ def __init__( limits = DEFAULT_CONNECTION_LIMITS if transport is not None: + kwargs["transport"] = transport warnings.warn( "The `transport` argument is deprecated. The `http_client` argument should be passed instead", category=DeprecationWarning, @@ -1379,6 +1383,7 @@ def __init__( raise ValueError("The `http_client` argument is mutually exclusive with `transport`") if proxies is not None: + kwargs["proxies"] = proxies warnings.warn( "The `proxies` argument is deprecated. The `http_client` argument should be passed instead", category=DeprecationWarning, @@ -1422,10 +1427,9 @@ def __init__( base_url=base_url, # cast to a valid type because mypy doesn't understand our type narrowing timeout=cast(Timeout, timeout), - proxies=proxies, - transport=transport, limits=limits, follow_redirects=True, + **kwargs, # type: ignore ) def is_closed(self) -> bool: From e9471086218734f1ae68331342c0b2cba2965ba1 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 29 Nov 2024 17:19:46 +0000 Subject: [PATCH 048/330] chore(internal): version bump (#72) --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/browserbase/_version.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index b7634f9b..80d368ad 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "1.0.3" + ".": "1.0.4" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index af4a0642..9db4126f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "browserbase" -version = "1.0.3" +version = "1.0.4" description = "The official Python library for the Browserbase API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/browserbase/_version.py b/src/browserbase/_version.py index f1870ed8..6e25ae84 100644 --- a/src/browserbase/_version.py +++ b/src/browserbase/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "browserbase" -__version__ = "1.0.3" # x-release-please-version +__version__ = "1.0.4" # x-release-please-version From 562911ca7102280f7fb280755a3fd1a9610d0871 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 3 Dec 2024 04:18:16 +0000 Subject: [PATCH 049/330] chore(internal): bump pyright (#73) --- requirements-dev.lock | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/requirements-dev.lock b/requirements-dev.lock index 968b92ed..f3f85d8b 100644 --- a/requirements-dev.lock +++ b/requirements-dev.lock @@ -68,7 +68,7 @@ pydantic-core==2.23.4 # via pydantic pygments==2.18.0 # via rich -pyright==1.1.380 +pyright==1.1.389 pytest==8.3.3 # via pytest-asyncio pytest-asyncio==0.24.0 @@ -97,6 +97,7 @@ typing-extensions==4.12.2 # via mypy # via pydantic # via pydantic-core + # via pyright virtualenv==20.24.5 # via nox zipp==3.17.0 From b7e16789db2d76d637eb4e1a516332db057ed67e Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 3 Dec 2024 17:02:17 +0000 Subject: [PATCH 050/330] chore(internal): version bump (#76) --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/browserbase/_version.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 80d368ad..1214610a 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "1.0.4" + ".": "1.0.5" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 9db4126f..0d0b06d7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "browserbase" -version = "1.0.4" +version = "1.0.5" description = "The official Python library for the Browserbase API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/browserbase/_version.py b/src/browserbase/_version.py index 6e25ae84..c871d01e 100644 --- a/src/browserbase/_version.py +++ b/src/browserbase/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "browserbase" -__version__ = "1.0.4" # x-release-please-version +__version__ = "1.0.5" # x-release-please-version From 0a165834a337f03ce5bb6cb02bab6e430df8eadf Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 4 Dec 2024 04:10:46 +0000 Subject: [PATCH 051/330] chore: make the `Omit` type public (#78) --- src/browserbase/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/browserbase/__init__.py b/src/browserbase/__init__.py index 4b1d2804..ce50c4ea 100644 --- a/src/browserbase/__init__.py +++ b/src/browserbase/__init__.py @@ -1,7 +1,7 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. from . import types -from ._types import NOT_GIVEN, NoneType, NotGiven, Transport, ProxiesTypes +from ._types import NOT_GIVEN, Omit, NoneType, NotGiven, Transport, ProxiesTypes from ._utils import file_from_path from ._client import ( Client, @@ -46,6 +46,7 @@ "ProxiesTypes", "NotGiven", "NOT_GIVEN", + "Omit", "BrowserbaseError", "APIError", "APIStatusError", From 0c61d7b73b614b375a22f08884d00b595e095895 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 10 Dec 2024 04:09:24 +0000 Subject: [PATCH 052/330] chore(internal): bump pydantic dependency (#81) --- requirements-dev.lock | 4 ++-- requirements.lock | 4 ++-- src/browserbase/_types.py | 6 ++---- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/requirements-dev.lock b/requirements-dev.lock index f3f85d8b..6f993022 100644 --- a/requirements-dev.lock +++ b/requirements-dev.lock @@ -62,9 +62,9 @@ platformdirs==3.11.0 # via virtualenv pluggy==1.5.0 # via pytest -pydantic==2.9.2 +pydantic==2.10.3 # via browserbase -pydantic-core==2.23.4 +pydantic-core==2.27.1 # via pydantic pygments==2.18.0 # via rich diff --git a/requirements.lock b/requirements.lock index 5f5acc29..25f6bb72 100644 --- a/requirements.lock +++ b/requirements.lock @@ -30,9 +30,9 @@ httpx==0.25.2 idna==3.4 # via anyio # via httpx -pydantic==2.9.2 +pydantic==2.10.3 # via browserbase -pydantic-core==2.23.4 +pydantic-core==2.27.1 # via pydantic sniffio==1.3.0 # via anyio diff --git a/src/browserbase/_types.py b/src/browserbase/_types.py index 1691090d..a8833dce 100644 --- a/src/browserbase/_types.py +++ b/src/browserbase/_types.py @@ -192,10 +192,8 @@ def get(self, __key: str) -> str | None: ... StrBytesIntFloat = Union[str, bytes, int, float] # Note: copied from Pydantic -# https://github.com/pydantic/pydantic/blob/32ea570bf96e84234d2992e1ddf40ab8a565925a/pydantic/main.py#L49 -IncEx: TypeAlias = Union[ - Set[int], Set[str], Mapping[int, Union["IncEx", Literal[True]]], Mapping[str, Union["IncEx", Literal[True]]] -] +# 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] From ca15591865df59daff67e6f38ca23f9af098e6ca Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 10 Dec 2024 04:14:19 +0000 Subject: [PATCH 053/330] docs(readme): fix http client proxies example (#82) --- README.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index d4d7e885..c00e27ff 100644 --- a/README.md +++ b/README.md @@ -276,18 +276,19 @@ can also get all the extra fields on the Pydantic model as a dict with 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 -- Custom transports +- 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 browserbase import Browserbase, DefaultHttpxClient client = Browserbase( # Or use the `BROWSERBASE_BASE_URL` env var base_url="http://my.test.server.example.com:8083", http_client=DefaultHttpxClient( - proxies="http://my.test.proxy.example.com", + proxy="http://my.test.proxy.example.com", transport=httpx.HTTPTransport(local_address="0.0.0.0"), ), ) From e0e9ea2e898b1e92acf75efaef50b8e4fa953f3e Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 13 Dec 2024 04:08:04 +0000 Subject: [PATCH 054/330] chore(internal): bump pyright (#83) --- requirements-dev.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.lock b/requirements-dev.lock index 6f993022..a4f35b3b 100644 --- a/requirements-dev.lock +++ b/requirements-dev.lock @@ -68,7 +68,7 @@ pydantic-core==2.27.1 # via pydantic pygments==2.18.0 # via rich -pyright==1.1.389 +pyright==1.1.390 pytest==8.3.3 # via pytest-asyncio pytest-asyncio==0.24.0 From bcb7be249c75487c7094e2905f56b766ab487036 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 13 Dec 2024 04:08:56 +0000 Subject: [PATCH 055/330] chore(internal): add support for TypeAliasType (#85) --- pyproject.toml | 2 +- src/browserbase/_models.py | 3 +++ src/browserbase/_response.py | 20 +++++++++---------- src/browserbase/_utils/__init__.py | 1 + src/browserbase/_utils/_typing.py | 31 +++++++++++++++++++++++++++++- tests/test_models.py | 18 ++++++++++++++++- tests/utils.py | 4 ++++ 7 files changed, 66 insertions(+), 13 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 0d0b06d7..9382cf7f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,7 +10,7 @@ authors = [ dependencies = [ "httpx>=0.23.0, <1", "pydantic>=1.9.0, <3", - "typing-extensions>=4.7, <5", + "typing-extensions>=4.10, <5", "anyio>=3.5.0, <5", "distro>=1.7.0, <2", "sniffio", diff --git a/src/browserbase/_models.py b/src/browserbase/_models.py index 6cb469e2..7a547ce5 100644 --- a/src/browserbase/_models.py +++ b/src/browserbase/_models.py @@ -46,6 +46,7 @@ strip_not_given, extract_type_arg, is_annotated_type, + is_type_alias_type, strip_annotated_type, ) from ._compat import ( @@ -428,6 +429,8 @@ def construct_type(*, value: object, type_: object) -> object: # 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_): + type_ = type_.__value__ # type: ignore[unreachable] # unwrap `Annotated[T, ...]` -> `T` if is_annotated_type(type_): diff --git a/src/browserbase/_response.py b/src/browserbase/_response.py index 81ae0828..18768050 100644 --- a/src/browserbase/_response.py +++ b/src/browserbase/_response.py @@ -25,7 +25,7 @@ import pydantic from ._types import NoneType -from ._utils import is_given, extract_type_arg, is_annotated_type, extract_type_var_from_base +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 @@ -126,9 +126,15 @@ def __repr__(self) -> str: ) 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 to and is_annotated_type(to): - to = extract_type_arg(to, 0) + if cast_to and is_annotated_type(cast_to): + cast_to = extract_type_arg(cast_to, 0) if self._is_sse_stream: if to: @@ -164,18 +170,12 @@ def _parse(self, *, to: type[_T] | None = None) -> R | _T: return cast( R, stream_cls( - cast_to=self._cast_to, + cast_to=cast_to, response=self.http_response, client=cast(Any, self._client), ), ) - cast_to = to if to is not None else self._cast_to - - # unwrap `Annotated[T, ...]` -> `T` - if is_annotated_type(cast_to): - cast_to = extract_type_arg(cast_to, 0) - if cast_to is NoneType: return cast(R, None) diff --git a/src/browserbase/_utils/__init__.py b/src/browserbase/_utils/__init__.py index a7cff3c0..d4fda26f 100644 --- a/src/browserbase/_utils/__init__.py +++ b/src/browserbase/_utils/__init__.py @@ -39,6 +39,7 @@ is_iterable_type as is_iterable_type, is_required_type as is_required_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, ) diff --git a/src/browserbase/_utils/_typing.py b/src/browserbase/_utils/_typing.py index c036991f..278749b1 100644 --- a/src/browserbase/_utils/_typing.py +++ b/src/browserbase/_utils/_typing.py @@ -1,8 +1,17 @@ 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 Required, Annotated, get_args, get_origin +from typing_extensions import ( + TypeIs, + Required, + Annotated, + get_args, + get_origin, +) from .._types import InheritsGeneric from .._compat import is_union as _is_union @@ -36,6 +45,26 @@ def is_typevar(typ: type) -> bool: 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, ...]] def strip_annotated_type(typ: type) -> type: if is_required_type(typ) or is_annotated_type(typ): diff --git a/tests/test_models.py b/tests/test_models.py index 37671b06..0db453cc 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -1,7 +1,7 @@ import json from typing import Any, Dict, List, Union, Optional, cast from datetime import datetime, timezone -from typing_extensions import Literal, Annotated +from typing_extensions import Literal, Annotated, TypeAliasType import pytest import pydantic @@ -828,3 +828,19 @@ class B(BaseModel): # if the discriminator details object stays the same between invocations then # we hit the cache assert UnionType.__discriminator__ is discriminator + + +@pytest.mark.skipif(not PYDANTIC_V2, reason="TypeAliasType is not supported in Pydantic v1") +def test_type_alias_type() -> None: + Alias = TypeAliasType("Alias", str) + + 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" diff --git a/tests/utils.py b/tests/utils.py index ad9be375..ac183a7e 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -16,6 +16,7 @@ is_union_type, extract_type_arg, is_annotated_type, + is_type_alias_type, ) from browserbase._compat import PYDANTIC_V2, field_outer_type, get_model_fields from browserbase._models import BaseModel @@ -51,6 +52,9 @@ def assert_matches_type( 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) From 63e97a93ee015d238c73dee47b48638a6373636e Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 14 Dec 2024 04:09:11 +0000 Subject: [PATCH 056/330] chore(internal): codegen related update (#86) --- src/browserbase/_client.py | 69 +++++++++++++++++++------------------- 1 file changed, 35 insertions(+), 34 deletions(-) diff --git a/src/browserbase/_client.py b/src/browserbase/_client.py index b3ed32f6..b845ea86 100644 --- a/src/browserbase/_client.py +++ b/src/browserbase/_client.py @@ -8,7 +8,7 @@ import httpx -from . import resources, _exceptions +from . import _exceptions from ._qs import Querystring from ._types import ( NOT_GIVEN, @@ -24,6 +24,7 @@ get_async_library, ) from ._version import __version__ +from .resources import contexts, projects, extensions from ._streaming import Stream as Stream, AsyncStream as AsyncStream from ._exceptions import APIStatusError, BrowserbaseError from ._base_client import ( @@ -31,13 +32,13 @@ SyncAPIClient, AsyncAPIClient, ) +from .resources.sessions import sessions __all__ = [ "Timeout", "Transport", "ProxiesTypes", "RequestOptions", - "resources", "Browserbase", "AsyncBrowserbase", "Client", @@ -46,10 +47,10 @@ class Browserbase(SyncAPIClient): - contexts: resources.ContextsResource - extensions: resources.ExtensionsResource - projects: resources.ProjectsResource - sessions: resources.SessionsResource + contexts: contexts.ContextsResource + extensions: extensions.ExtensionsResource + projects: projects.ProjectsResource + sessions: sessions.SessionsResource with_raw_response: BrowserbaseWithRawResponse with_streaming_response: BrowserbaseWithStreamedResponse @@ -107,10 +108,10 @@ def __init__( _strict_response_validation=_strict_response_validation, ) - self.contexts = resources.ContextsResource(self) - self.extensions = resources.ExtensionsResource(self) - self.projects = resources.ProjectsResource(self) - self.sessions = resources.SessionsResource(self) + self.contexts = contexts.ContextsResource(self) + self.extensions = extensions.ExtensionsResource(self) + self.projects = projects.ProjectsResource(self) + self.sessions = sessions.SessionsResource(self) self.with_raw_response = BrowserbaseWithRawResponse(self) self.with_streaming_response = BrowserbaseWithStreamedResponse(self) @@ -220,10 +221,10 @@ def _make_status_error( class AsyncBrowserbase(AsyncAPIClient): - contexts: resources.AsyncContextsResource - extensions: resources.AsyncExtensionsResource - projects: resources.AsyncProjectsResource - sessions: resources.AsyncSessionsResource + contexts: contexts.AsyncContextsResource + extensions: extensions.AsyncExtensionsResource + projects: projects.AsyncProjectsResource + sessions: sessions.AsyncSessionsResource with_raw_response: AsyncBrowserbaseWithRawResponse with_streaming_response: AsyncBrowserbaseWithStreamedResponse @@ -281,10 +282,10 @@ def __init__( _strict_response_validation=_strict_response_validation, ) - self.contexts = resources.AsyncContextsResource(self) - self.extensions = resources.AsyncExtensionsResource(self) - self.projects = resources.AsyncProjectsResource(self) - self.sessions = resources.AsyncSessionsResource(self) + self.contexts = contexts.AsyncContextsResource(self) + self.extensions = extensions.AsyncExtensionsResource(self) + self.projects = projects.AsyncProjectsResource(self) + self.sessions = sessions.AsyncSessionsResource(self) self.with_raw_response = AsyncBrowserbaseWithRawResponse(self) self.with_streaming_response = AsyncBrowserbaseWithStreamedResponse(self) @@ -395,34 +396,34 @@ def _make_status_error( class BrowserbaseWithRawResponse: def __init__(self, client: Browserbase) -> None: - self.contexts = resources.ContextsResourceWithRawResponse(client.contexts) - self.extensions = resources.ExtensionsResourceWithRawResponse(client.extensions) - self.projects = resources.ProjectsResourceWithRawResponse(client.projects) - self.sessions = resources.SessionsResourceWithRawResponse(client.sessions) + self.contexts = contexts.ContextsResourceWithRawResponse(client.contexts) + self.extensions = extensions.ExtensionsResourceWithRawResponse(client.extensions) + self.projects = projects.ProjectsResourceWithRawResponse(client.projects) + self.sessions = sessions.SessionsResourceWithRawResponse(client.sessions) class AsyncBrowserbaseWithRawResponse: def __init__(self, client: AsyncBrowserbase) -> None: - self.contexts = resources.AsyncContextsResourceWithRawResponse(client.contexts) - self.extensions = resources.AsyncExtensionsResourceWithRawResponse(client.extensions) - self.projects = resources.AsyncProjectsResourceWithRawResponse(client.projects) - self.sessions = resources.AsyncSessionsResourceWithRawResponse(client.sessions) + self.contexts = contexts.AsyncContextsResourceWithRawResponse(client.contexts) + self.extensions = extensions.AsyncExtensionsResourceWithRawResponse(client.extensions) + self.projects = projects.AsyncProjectsResourceWithRawResponse(client.projects) + self.sessions = sessions.AsyncSessionsResourceWithRawResponse(client.sessions) class BrowserbaseWithStreamedResponse: def __init__(self, client: Browserbase) -> None: - self.contexts = resources.ContextsResourceWithStreamingResponse(client.contexts) - self.extensions = resources.ExtensionsResourceWithStreamingResponse(client.extensions) - self.projects = resources.ProjectsResourceWithStreamingResponse(client.projects) - self.sessions = resources.SessionsResourceWithStreamingResponse(client.sessions) + self.contexts = contexts.ContextsResourceWithStreamingResponse(client.contexts) + self.extensions = extensions.ExtensionsResourceWithStreamingResponse(client.extensions) + self.projects = projects.ProjectsResourceWithStreamingResponse(client.projects) + self.sessions = sessions.SessionsResourceWithStreamingResponse(client.sessions) class AsyncBrowserbaseWithStreamedResponse: def __init__(self, client: AsyncBrowserbase) -> None: - self.contexts = resources.AsyncContextsResourceWithStreamingResponse(client.contexts) - self.extensions = resources.AsyncExtensionsResourceWithStreamingResponse(client.extensions) - self.projects = resources.AsyncProjectsResourceWithStreamingResponse(client.projects) - self.sessions = resources.AsyncSessionsResourceWithStreamingResponse(client.sessions) + self.contexts = contexts.AsyncContextsResourceWithStreamingResponse(client.contexts) + self.extensions = extensions.AsyncExtensionsResourceWithStreamingResponse(client.extensions) + self.projects = projects.AsyncProjectsResourceWithStreamingResponse(client.projects) + self.sessions = sessions.AsyncSessionsResourceWithStreamingResponse(client.sessions) Client = Browserbase From 46a772e767744d119fe46e304f2bf8e885a4c431 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 14 Dec 2024 04:09:51 +0000 Subject: [PATCH 057/330] chore(internal): codegen related update (#87) --- src/browserbase/_client.py | 69 +++++++++++++++++++------------------- 1 file changed, 34 insertions(+), 35 deletions(-) diff --git a/src/browserbase/_client.py b/src/browserbase/_client.py index b845ea86..b3ed32f6 100644 --- a/src/browserbase/_client.py +++ b/src/browserbase/_client.py @@ -8,7 +8,7 @@ import httpx -from . import _exceptions +from . import resources, _exceptions from ._qs import Querystring from ._types import ( NOT_GIVEN, @@ -24,7 +24,6 @@ get_async_library, ) from ._version import __version__ -from .resources import contexts, projects, extensions from ._streaming import Stream as Stream, AsyncStream as AsyncStream from ._exceptions import APIStatusError, BrowserbaseError from ._base_client import ( @@ -32,13 +31,13 @@ SyncAPIClient, AsyncAPIClient, ) -from .resources.sessions import sessions __all__ = [ "Timeout", "Transport", "ProxiesTypes", "RequestOptions", + "resources", "Browserbase", "AsyncBrowserbase", "Client", @@ -47,10 +46,10 @@ class Browserbase(SyncAPIClient): - contexts: contexts.ContextsResource - extensions: extensions.ExtensionsResource - projects: projects.ProjectsResource - sessions: sessions.SessionsResource + contexts: resources.ContextsResource + extensions: resources.ExtensionsResource + projects: resources.ProjectsResource + sessions: resources.SessionsResource with_raw_response: BrowserbaseWithRawResponse with_streaming_response: BrowserbaseWithStreamedResponse @@ -108,10 +107,10 @@ def __init__( _strict_response_validation=_strict_response_validation, ) - self.contexts = contexts.ContextsResource(self) - self.extensions = extensions.ExtensionsResource(self) - self.projects = projects.ProjectsResource(self) - self.sessions = sessions.SessionsResource(self) + self.contexts = resources.ContextsResource(self) + self.extensions = resources.ExtensionsResource(self) + self.projects = resources.ProjectsResource(self) + self.sessions = resources.SessionsResource(self) self.with_raw_response = BrowserbaseWithRawResponse(self) self.with_streaming_response = BrowserbaseWithStreamedResponse(self) @@ -221,10 +220,10 @@ def _make_status_error( class AsyncBrowserbase(AsyncAPIClient): - contexts: contexts.AsyncContextsResource - extensions: extensions.AsyncExtensionsResource - projects: projects.AsyncProjectsResource - sessions: sessions.AsyncSessionsResource + contexts: resources.AsyncContextsResource + extensions: resources.AsyncExtensionsResource + projects: resources.AsyncProjectsResource + sessions: resources.AsyncSessionsResource with_raw_response: AsyncBrowserbaseWithRawResponse with_streaming_response: AsyncBrowserbaseWithStreamedResponse @@ -282,10 +281,10 @@ def __init__( _strict_response_validation=_strict_response_validation, ) - self.contexts = contexts.AsyncContextsResource(self) - self.extensions = extensions.AsyncExtensionsResource(self) - self.projects = projects.AsyncProjectsResource(self) - self.sessions = sessions.AsyncSessionsResource(self) + self.contexts = resources.AsyncContextsResource(self) + self.extensions = resources.AsyncExtensionsResource(self) + self.projects = resources.AsyncProjectsResource(self) + self.sessions = resources.AsyncSessionsResource(self) self.with_raw_response = AsyncBrowserbaseWithRawResponse(self) self.with_streaming_response = AsyncBrowserbaseWithStreamedResponse(self) @@ -396,34 +395,34 @@ def _make_status_error( class BrowserbaseWithRawResponse: def __init__(self, client: Browserbase) -> None: - self.contexts = contexts.ContextsResourceWithRawResponse(client.contexts) - self.extensions = extensions.ExtensionsResourceWithRawResponse(client.extensions) - self.projects = projects.ProjectsResourceWithRawResponse(client.projects) - self.sessions = sessions.SessionsResourceWithRawResponse(client.sessions) + self.contexts = resources.ContextsResourceWithRawResponse(client.contexts) + self.extensions = resources.ExtensionsResourceWithRawResponse(client.extensions) + self.projects = resources.ProjectsResourceWithRawResponse(client.projects) + self.sessions = resources.SessionsResourceWithRawResponse(client.sessions) class AsyncBrowserbaseWithRawResponse: def __init__(self, client: AsyncBrowserbase) -> None: - self.contexts = contexts.AsyncContextsResourceWithRawResponse(client.contexts) - self.extensions = extensions.AsyncExtensionsResourceWithRawResponse(client.extensions) - self.projects = projects.AsyncProjectsResourceWithRawResponse(client.projects) - self.sessions = sessions.AsyncSessionsResourceWithRawResponse(client.sessions) + self.contexts = resources.AsyncContextsResourceWithRawResponse(client.contexts) + self.extensions = resources.AsyncExtensionsResourceWithRawResponse(client.extensions) + self.projects = resources.AsyncProjectsResourceWithRawResponse(client.projects) + self.sessions = resources.AsyncSessionsResourceWithRawResponse(client.sessions) class BrowserbaseWithStreamedResponse: def __init__(self, client: Browserbase) -> None: - self.contexts = contexts.ContextsResourceWithStreamingResponse(client.contexts) - self.extensions = extensions.ExtensionsResourceWithStreamingResponse(client.extensions) - self.projects = projects.ProjectsResourceWithStreamingResponse(client.projects) - self.sessions = sessions.SessionsResourceWithStreamingResponse(client.sessions) + self.contexts = resources.ContextsResourceWithStreamingResponse(client.contexts) + self.extensions = resources.ExtensionsResourceWithStreamingResponse(client.extensions) + self.projects = resources.ProjectsResourceWithStreamingResponse(client.projects) + self.sessions = resources.SessionsResourceWithStreamingResponse(client.sessions) class AsyncBrowserbaseWithStreamedResponse: def __init__(self, client: AsyncBrowserbase) -> None: - self.contexts = contexts.AsyncContextsResourceWithStreamingResponse(client.contexts) - self.extensions = extensions.AsyncExtensionsResourceWithStreamingResponse(client.extensions) - self.projects = projects.AsyncProjectsResourceWithStreamingResponse(client.projects) - self.sessions = sessions.AsyncSessionsResourceWithStreamingResponse(client.sessions) + self.contexts = resources.AsyncContextsResourceWithStreamingResponse(client.contexts) + self.extensions = resources.AsyncExtensionsResourceWithStreamingResponse(client.extensions) + self.projects = resources.AsyncProjectsResourceWithStreamingResponse(client.projects) + self.sessions = resources.AsyncSessionsResourceWithStreamingResponse(client.sessions) Client = Browserbase From 91f0ca736f5f8f5fa4411c67c548e4835b27a2ea Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 14 Dec 2024 04:22:12 +0000 Subject: [PATCH 058/330] chore(internal): codegen related update (#88) --- src/browserbase/_client.py | 69 +++++++++++++++++++------------------- 1 file changed, 35 insertions(+), 34 deletions(-) diff --git a/src/browserbase/_client.py b/src/browserbase/_client.py index b3ed32f6..b845ea86 100644 --- a/src/browserbase/_client.py +++ b/src/browserbase/_client.py @@ -8,7 +8,7 @@ import httpx -from . import resources, _exceptions +from . import _exceptions from ._qs import Querystring from ._types import ( NOT_GIVEN, @@ -24,6 +24,7 @@ get_async_library, ) from ._version import __version__ +from .resources import contexts, projects, extensions from ._streaming import Stream as Stream, AsyncStream as AsyncStream from ._exceptions import APIStatusError, BrowserbaseError from ._base_client import ( @@ -31,13 +32,13 @@ SyncAPIClient, AsyncAPIClient, ) +from .resources.sessions import sessions __all__ = [ "Timeout", "Transport", "ProxiesTypes", "RequestOptions", - "resources", "Browserbase", "AsyncBrowserbase", "Client", @@ -46,10 +47,10 @@ class Browserbase(SyncAPIClient): - contexts: resources.ContextsResource - extensions: resources.ExtensionsResource - projects: resources.ProjectsResource - sessions: resources.SessionsResource + contexts: contexts.ContextsResource + extensions: extensions.ExtensionsResource + projects: projects.ProjectsResource + sessions: sessions.SessionsResource with_raw_response: BrowserbaseWithRawResponse with_streaming_response: BrowserbaseWithStreamedResponse @@ -107,10 +108,10 @@ def __init__( _strict_response_validation=_strict_response_validation, ) - self.contexts = resources.ContextsResource(self) - self.extensions = resources.ExtensionsResource(self) - self.projects = resources.ProjectsResource(self) - self.sessions = resources.SessionsResource(self) + self.contexts = contexts.ContextsResource(self) + self.extensions = extensions.ExtensionsResource(self) + self.projects = projects.ProjectsResource(self) + self.sessions = sessions.SessionsResource(self) self.with_raw_response = BrowserbaseWithRawResponse(self) self.with_streaming_response = BrowserbaseWithStreamedResponse(self) @@ -220,10 +221,10 @@ def _make_status_error( class AsyncBrowserbase(AsyncAPIClient): - contexts: resources.AsyncContextsResource - extensions: resources.AsyncExtensionsResource - projects: resources.AsyncProjectsResource - sessions: resources.AsyncSessionsResource + contexts: contexts.AsyncContextsResource + extensions: extensions.AsyncExtensionsResource + projects: projects.AsyncProjectsResource + sessions: sessions.AsyncSessionsResource with_raw_response: AsyncBrowserbaseWithRawResponse with_streaming_response: AsyncBrowserbaseWithStreamedResponse @@ -281,10 +282,10 @@ def __init__( _strict_response_validation=_strict_response_validation, ) - self.contexts = resources.AsyncContextsResource(self) - self.extensions = resources.AsyncExtensionsResource(self) - self.projects = resources.AsyncProjectsResource(self) - self.sessions = resources.AsyncSessionsResource(self) + self.contexts = contexts.AsyncContextsResource(self) + self.extensions = extensions.AsyncExtensionsResource(self) + self.projects = projects.AsyncProjectsResource(self) + self.sessions = sessions.AsyncSessionsResource(self) self.with_raw_response = AsyncBrowserbaseWithRawResponse(self) self.with_streaming_response = AsyncBrowserbaseWithStreamedResponse(self) @@ -395,34 +396,34 @@ def _make_status_error( class BrowserbaseWithRawResponse: def __init__(self, client: Browserbase) -> None: - self.contexts = resources.ContextsResourceWithRawResponse(client.contexts) - self.extensions = resources.ExtensionsResourceWithRawResponse(client.extensions) - self.projects = resources.ProjectsResourceWithRawResponse(client.projects) - self.sessions = resources.SessionsResourceWithRawResponse(client.sessions) + self.contexts = contexts.ContextsResourceWithRawResponse(client.contexts) + self.extensions = extensions.ExtensionsResourceWithRawResponse(client.extensions) + self.projects = projects.ProjectsResourceWithRawResponse(client.projects) + self.sessions = sessions.SessionsResourceWithRawResponse(client.sessions) class AsyncBrowserbaseWithRawResponse: def __init__(self, client: AsyncBrowserbase) -> None: - self.contexts = resources.AsyncContextsResourceWithRawResponse(client.contexts) - self.extensions = resources.AsyncExtensionsResourceWithRawResponse(client.extensions) - self.projects = resources.AsyncProjectsResourceWithRawResponse(client.projects) - self.sessions = resources.AsyncSessionsResourceWithRawResponse(client.sessions) + self.contexts = contexts.AsyncContextsResourceWithRawResponse(client.contexts) + self.extensions = extensions.AsyncExtensionsResourceWithRawResponse(client.extensions) + self.projects = projects.AsyncProjectsResourceWithRawResponse(client.projects) + self.sessions = sessions.AsyncSessionsResourceWithRawResponse(client.sessions) class BrowserbaseWithStreamedResponse: def __init__(self, client: Browserbase) -> None: - self.contexts = resources.ContextsResourceWithStreamingResponse(client.contexts) - self.extensions = resources.ExtensionsResourceWithStreamingResponse(client.extensions) - self.projects = resources.ProjectsResourceWithStreamingResponse(client.projects) - self.sessions = resources.SessionsResourceWithStreamingResponse(client.sessions) + self.contexts = contexts.ContextsResourceWithStreamingResponse(client.contexts) + self.extensions = extensions.ExtensionsResourceWithStreamingResponse(client.extensions) + self.projects = projects.ProjectsResourceWithStreamingResponse(client.projects) + self.sessions = sessions.SessionsResourceWithStreamingResponse(client.sessions) class AsyncBrowserbaseWithStreamedResponse: def __init__(self, client: AsyncBrowserbase) -> None: - self.contexts = resources.AsyncContextsResourceWithStreamingResponse(client.contexts) - self.extensions = resources.AsyncExtensionsResourceWithStreamingResponse(client.extensions) - self.projects = resources.AsyncProjectsResourceWithStreamingResponse(client.projects) - self.sessions = resources.AsyncSessionsResourceWithStreamingResponse(client.sessions) + self.contexts = contexts.AsyncContextsResourceWithStreamingResponse(client.contexts) + self.extensions = extensions.AsyncExtensionsResourceWithStreamingResponse(client.extensions) + self.projects = projects.AsyncProjectsResourceWithStreamingResponse(client.projects) + self.sessions = sessions.AsyncSessionsResourceWithStreamingResponse(client.sessions) Client = Browserbase From 701aabd654869926d4e63abb0ca56bc656a59d5a Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 14 Dec 2024 04:22:50 +0000 Subject: [PATCH 059/330] chore(internal): remove some duplicated imports (#89) --- src/browserbase/_client.py | 69 +++++++++++++++++++------------------- 1 file changed, 34 insertions(+), 35 deletions(-) diff --git a/src/browserbase/_client.py b/src/browserbase/_client.py index b845ea86..b3ed32f6 100644 --- a/src/browserbase/_client.py +++ b/src/browserbase/_client.py @@ -8,7 +8,7 @@ import httpx -from . import _exceptions +from . import resources, _exceptions from ._qs import Querystring from ._types import ( NOT_GIVEN, @@ -24,7 +24,6 @@ get_async_library, ) from ._version import __version__ -from .resources import contexts, projects, extensions from ._streaming import Stream as Stream, AsyncStream as AsyncStream from ._exceptions import APIStatusError, BrowserbaseError from ._base_client import ( @@ -32,13 +31,13 @@ SyncAPIClient, AsyncAPIClient, ) -from .resources.sessions import sessions __all__ = [ "Timeout", "Transport", "ProxiesTypes", "RequestOptions", + "resources", "Browserbase", "AsyncBrowserbase", "Client", @@ -47,10 +46,10 @@ class Browserbase(SyncAPIClient): - contexts: contexts.ContextsResource - extensions: extensions.ExtensionsResource - projects: projects.ProjectsResource - sessions: sessions.SessionsResource + contexts: resources.ContextsResource + extensions: resources.ExtensionsResource + projects: resources.ProjectsResource + sessions: resources.SessionsResource with_raw_response: BrowserbaseWithRawResponse with_streaming_response: BrowserbaseWithStreamedResponse @@ -108,10 +107,10 @@ def __init__( _strict_response_validation=_strict_response_validation, ) - self.contexts = contexts.ContextsResource(self) - self.extensions = extensions.ExtensionsResource(self) - self.projects = projects.ProjectsResource(self) - self.sessions = sessions.SessionsResource(self) + self.contexts = resources.ContextsResource(self) + self.extensions = resources.ExtensionsResource(self) + self.projects = resources.ProjectsResource(self) + self.sessions = resources.SessionsResource(self) self.with_raw_response = BrowserbaseWithRawResponse(self) self.with_streaming_response = BrowserbaseWithStreamedResponse(self) @@ -221,10 +220,10 @@ def _make_status_error( class AsyncBrowserbase(AsyncAPIClient): - contexts: contexts.AsyncContextsResource - extensions: extensions.AsyncExtensionsResource - projects: projects.AsyncProjectsResource - sessions: sessions.AsyncSessionsResource + contexts: resources.AsyncContextsResource + extensions: resources.AsyncExtensionsResource + projects: resources.AsyncProjectsResource + sessions: resources.AsyncSessionsResource with_raw_response: AsyncBrowserbaseWithRawResponse with_streaming_response: AsyncBrowserbaseWithStreamedResponse @@ -282,10 +281,10 @@ def __init__( _strict_response_validation=_strict_response_validation, ) - self.contexts = contexts.AsyncContextsResource(self) - self.extensions = extensions.AsyncExtensionsResource(self) - self.projects = projects.AsyncProjectsResource(self) - self.sessions = sessions.AsyncSessionsResource(self) + self.contexts = resources.AsyncContextsResource(self) + self.extensions = resources.AsyncExtensionsResource(self) + self.projects = resources.AsyncProjectsResource(self) + self.sessions = resources.AsyncSessionsResource(self) self.with_raw_response = AsyncBrowserbaseWithRawResponse(self) self.with_streaming_response = AsyncBrowserbaseWithStreamedResponse(self) @@ -396,34 +395,34 @@ def _make_status_error( class BrowserbaseWithRawResponse: def __init__(self, client: Browserbase) -> None: - self.contexts = contexts.ContextsResourceWithRawResponse(client.contexts) - self.extensions = extensions.ExtensionsResourceWithRawResponse(client.extensions) - self.projects = projects.ProjectsResourceWithRawResponse(client.projects) - self.sessions = sessions.SessionsResourceWithRawResponse(client.sessions) + self.contexts = resources.ContextsResourceWithRawResponse(client.contexts) + self.extensions = resources.ExtensionsResourceWithRawResponse(client.extensions) + self.projects = resources.ProjectsResourceWithRawResponse(client.projects) + self.sessions = resources.SessionsResourceWithRawResponse(client.sessions) class AsyncBrowserbaseWithRawResponse: def __init__(self, client: AsyncBrowserbase) -> None: - self.contexts = contexts.AsyncContextsResourceWithRawResponse(client.contexts) - self.extensions = extensions.AsyncExtensionsResourceWithRawResponse(client.extensions) - self.projects = projects.AsyncProjectsResourceWithRawResponse(client.projects) - self.sessions = sessions.AsyncSessionsResourceWithRawResponse(client.sessions) + self.contexts = resources.AsyncContextsResourceWithRawResponse(client.contexts) + self.extensions = resources.AsyncExtensionsResourceWithRawResponse(client.extensions) + self.projects = resources.AsyncProjectsResourceWithRawResponse(client.projects) + self.sessions = resources.AsyncSessionsResourceWithRawResponse(client.sessions) class BrowserbaseWithStreamedResponse: def __init__(self, client: Browserbase) -> None: - self.contexts = contexts.ContextsResourceWithStreamingResponse(client.contexts) - self.extensions = extensions.ExtensionsResourceWithStreamingResponse(client.extensions) - self.projects = projects.ProjectsResourceWithStreamingResponse(client.projects) - self.sessions = sessions.SessionsResourceWithStreamingResponse(client.sessions) + self.contexts = resources.ContextsResourceWithStreamingResponse(client.contexts) + self.extensions = resources.ExtensionsResourceWithStreamingResponse(client.extensions) + self.projects = resources.ProjectsResourceWithStreamingResponse(client.projects) + self.sessions = resources.SessionsResourceWithStreamingResponse(client.sessions) class AsyncBrowserbaseWithStreamedResponse: def __init__(self, client: AsyncBrowserbase) -> None: - self.contexts = contexts.AsyncContextsResourceWithStreamingResponse(client.contexts) - self.extensions = extensions.AsyncExtensionsResourceWithStreamingResponse(client.extensions) - self.projects = projects.AsyncProjectsResourceWithStreamingResponse(client.projects) - self.sessions = sessions.AsyncSessionsResourceWithStreamingResponse(client.sessions) + self.contexts = resources.AsyncContextsResourceWithStreamingResponse(client.contexts) + self.extensions = resources.AsyncExtensionsResourceWithStreamingResponse(client.extensions) + self.projects = resources.AsyncProjectsResourceWithStreamingResponse(client.projects) + self.sessions = resources.AsyncSessionsResourceWithStreamingResponse(client.sessions) Client = Browserbase From 0a91605ee703e1f6415051445314d034bad9d661 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 14 Dec 2024 04:23:31 +0000 Subject: [PATCH 060/330] chore(internal): updated imports (#90) --- src/browserbase/_client.py | 69 +++++++++++++++++++------------------- 1 file changed, 35 insertions(+), 34 deletions(-) diff --git a/src/browserbase/_client.py b/src/browserbase/_client.py index b3ed32f6..b845ea86 100644 --- a/src/browserbase/_client.py +++ b/src/browserbase/_client.py @@ -8,7 +8,7 @@ import httpx -from . import resources, _exceptions +from . import _exceptions from ._qs import Querystring from ._types import ( NOT_GIVEN, @@ -24,6 +24,7 @@ get_async_library, ) from ._version import __version__ +from .resources import contexts, projects, extensions from ._streaming import Stream as Stream, AsyncStream as AsyncStream from ._exceptions import APIStatusError, BrowserbaseError from ._base_client import ( @@ -31,13 +32,13 @@ SyncAPIClient, AsyncAPIClient, ) +from .resources.sessions import sessions __all__ = [ "Timeout", "Transport", "ProxiesTypes", "RequestOptions", - "resources", "Browserbase", "AsyncBrowserbase", "Client", @@ -46,10 +47,10 @@ class Browserbase(SyncAPIClient): - contexts: resources.ContextsResource - extensions: resources.ExtensionsResource - projects: resources.ProjectsResource - sessions: resources.SessionsResource + contexts: contexts.ContextsResource + extensions: extensions.ExtensionsResource + projects: projects.ProjectsResource + sessions: sessions.SessionsResource with_raw_response: BrowserbaseWithRawResponse with_streaming_response: BrowserbaseWithStreamedResponse @@ -107,10 +108,10 @@ def __init__( _strict_response_validation=_strict_response_validation, ) - self.contexts = resources.ContextsResource(self) - self.extensions = resources.ExtensionsResource(self) - self.projects = resources.ProjectsResource(self) - self.sessions = resources.SessionsResource(self) + self.contexts = contexts.ContextsResource(self) + self.extensions = extensions.ExtensionsResource(self) + self.projects = projects.ProjectsResource(self) + self.sessions = sessions.SessionsResource(self) self.with_raw_response = BrowserbaseWithRawResponse(self) self.with_streaming_response = BrowserbaseWithStreamedResponse(self) @@ -220,10 +221,10 @@ def _make_status_error( class AsyncBrowserbase(AsyncAPIClient): - contexts: resources.AsyncContextsResource - extensions: resources.AsyncExtensionsResource - projects: resources.AsyncProjectsResource - sessions: resources.AsyncSessionsResource + contexts: contexts.AsyncContextsResource + extensions: extensions.AsyncExtensionsResource + projects: projects.AsyncProjectsResource + sessions: sessions.AsyncSessionsResource with_raw_response: AsyncBrowserbaseWithRawResponse with_streaming_response: AsyncBrowserbaseWithStreamedResponse @@ -281,10 +282,10 @@ def __init__( _strict_response_validation=_strict_response_validation, ) - self.contexts = resources.AsyncContextsResource(self) - self.extensions = resources.AsyncExtensionsResource(self) - self.projects = resources.AsyncProjectsResource(self) - self.sessions = resources.AsyncSessionsResource(self) + self.contexts = contexts.AsyncContextsResource(self) + self.extensions = extensions.AsyncExtensionsResource(self) + self.projects = projects.AsyncProjectsResource(self) + self.sessions = sessions.AsyncSessionsResource(self) self.with_raw_response = AsyncBrowserbaseWithRawResponse(self) self.with_streaming_response = AsyncBrowserbaseWithStreamedResponse(self) @@ -395,34 +396,34 @@ def _make_status_error( class BrowserbaseWithRawResponse: def __init__(self, client: Browserbase) -> None: - self.contexts = resources.ContextsResourceWithRawResponse(client.contexts) - self.extensions = resources.ExtensionsResourceWithRawResponse(client.extensions) - self.projects = resources.ProjectsResourceWithRawResponse(client.projects) - self.sessions = resources.SessionsResourceWithRawResponse(client.sessions) + self.contexts = contexts.ContextsResourceWithRawResponse(client.contexts) + self.extensions = extensions.ExtensionsResourceWithRawResponse(client.extensions) + self.projects = projects.ProjectsResourceWithRawResponse(client.projects) + self.sessions = sessions.SessionsResourceWithRawResponse(client.sessions) class AsyncBrowserbaseWithRawResponse: def __init__(self, client: AsyncBrowserbase) -> None: - self.contexts = resources.AsyncContextsResourceWithRawResponse(client.contexts) - self.extensions = resources.AsyncExtensionsResourceWithRawResponse(client.extensions) - self.projects = resources.AsyncProjectsResourceWithRawResponse(client.projects) - self.sessions = resources.AsyncSessionsResourceWithRawResponse(client.sessions) + self.contexts = contexts.AsyncContextsResourceWithRawResponse(client.contexts) + self.extensions = extensions.AsyncExtensionsResourceWithRawResponse(client.extensions) + self.projects = projects.AsyncProjectsResourceWithRawResponse(client.projects) + self.sessions = sessions.AsyncSessionsResourceWithRawResponse(client.sessions) class BrowserbaseWithStreamedResponse: def __init__(self, client: Browserbase) -> None: - self.contexts = resources.ContextsResourceWithStreamingResponse(client.contexts) - self.extensions = resources.ExtensionsResourceWithStreamingResponse(client.extensions) - self.projects = resources.ProjectsResourceWithStreamingResponse(client.projects) - self.sessions = resources.SessionsResourceWithStreamingResponse(client.sessions) + self.contexts = contexts.ContextsResourceWithStreamingResponse(client.contexts) + self.extensions = extensions.ExtensionsResourceWithStreamingResponse(client.extensions) + self.projects = projects.ProjectsResourceWithStreamingResponse(client.projects) + self.sessions = sessions.SessionsResourceWithStreamingResponse(client.sessions) class AsyncBrowserbaseWithStreamedResponse: def __init__(self, client: AsyncBrowserbase) -> None: - self.contexts = resources.AsyncContextsResourceWithStreamingResponse(client.contexts) - self.extensions = resources.AsyncExtensionsResourceWithStreamingResponse(client.extensions) - self.projects = resources.AsyncProjectsResourceWithStreamingResponse(client.projects) - self.sessions = resources.AsyncSessionsResourceWithStreamingResponse(client.sessions) + self.contexts = contexts.AsyncContextsResourceWithStreamingResponse(client.contexts) + self.extensions = extensions.AsyncExtensionsResourceWithStreamingResponse(client.extensions) + self.projects = projects.AsyncProjectsResourceWithStreamingResponse(client.projects) + self.sessions = sessions.AsyncSessionsResourceWithStreamingResponse(client.sessions) Client = Browserbase From 859243e3be1c6e28c1f581d3da74a9e8268b1645 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 17 Dec 2024 04:18:06 +0000 Subject: [PATCH 061/330] docs(readme): example snippet for client context manager (#91) --- README.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/README.md b/README.md index c00e27ff..95cb49b6 100644 --- a/README.md +++ b/README.md @@ -304,6 +304,16 @@ 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 browserbase import Browserbase + +with Browserbase() 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: From c77ae5758a1f9a1eddceb885e60e689e3cb73f41 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 18 Dec 2024 04:19:26 +0000 Subject: [PATCH 062/330] chore(internal): fix some typos (#92) --- tests/test_client.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/test_client.py b/tests/test_client.py index aa0043e2..776b48b5 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -355,11 +355,11 @@ def test_default_query_option(self) -> None: FinalRequestOptions( method="get", url="/foo", - params={"foo": "baz", "query_param": "overriden"}, + params={"foo": "baz", "query_param": "overridden"}, ) ) url = httpx.URL(request.url) - assert dict(url.params) == {"foo": "baz", "query_param": "overriden"} + assert dict(url.params) == {"foo": "baz", "query_param": "overridden"} def test_request_extra_json(self) -> None: request = self.client._build_request( @@ -1131,11 +1131,11 @@ def test_default_query_option(self) -> None: FinalRequestOptions( method="get", url="/foo", - params={"foo": "baz", "query_param": "overriden"}, + params={"foo": "baz", "query_param": "overridden"}, ) ) url = httpx.URL(request.url) - assert dict(url.params) == {"foo": "baz", "query_param": "overriden"} + assert dict(url.params) == {"foo": "baz", "query_param": "overridden"} def test_request_extra_json(self) -> None: request = self.client._build_request( From 141a03212bf5426c17c6137de0f68a7c00c34b39 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 2 Jan 2025 04:25:19 +0000 Subject: [PATCH 063/330] chore(internal): codegen related update (#93) --- LICENSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE b/LICENSE index 915e6f84..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 2024 Browserbase + 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. From 1df15f1125a7a1c4d85dc5de8053bbc35b3c9bb1 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 7 Jan 2025 04:34:28 +0000 Subject: [PATCH 064/330] chore: add missing isclass check (#94) --- src/browserbase/_models.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/browserbase/_models.py b/src/browserbase/_models.py index 7a547ce5..d56ea1d9 100644 --- a/src/browserbase/_models.py +++ b/src/browserbase/_models.py @@ -488,7 +488,11 @@ def construct_type(*, value: object, type_: object) -> object: _, 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 (issubclass(origin, BaseModel) or issubclass(origin, GenericModel)): + 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] From 0e7fcaf0523dbef1eb62c9629cb0f0a9a34990ba Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 8 Jan 2025 04:29:16 +0000 Subject: [PATCH 065/330] chore(internal): bump httpx dependency (#95) --- pyproject.toml | 2 +- requirements-dev.lock | 5 ++--- requirements.lock | 3 +-- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 9382cf7f..a1a1e201 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -54,7 +54,7 @@ dev-dependencies = [ "dirty-equals>=0.6.0", "importlib-metadata>=6.7.0", "rich>=13.7.1", - "nest_asyncio==1.6.0" + "nest_asyncio==1.6.0", ] [tool.rye.scripts] diff --git a/requirements-dev.lock b/requirements-dev.lock index a4f35b3b..a9c9df5c 100644 --- a/requirements-dev.lock +++ b/requirements-dev.lock @@ -35,7 +35,7 @@ h11==0.14.0 # via httpcore httpcore==1.0.2 # via httpx -httpx==0.25.2 +httpx==0.28.1 # via browserbase # via respx idna==3.4 @@ -76,7 +76,7 @@ python-dateutil==2.8.2 # via time-machine pytz==2023.3.post1 # via dirty-equals -respx==0.20.2 +respx==0.22.0 rich==13.7.1 ruff==0.6.9 setuptools==68.2.2 @@ -86,7 +86,6 @@ six==1.16.0 sniffio==1.3.0 # via anyio # via browserbase - # via httpx time-machine==2.9.0 tomli==2.0.2 # via mypy diff --git a/requirements.lock b/requirements.lock index 25f6bb72..fea30548 100644 --- a/requirements.lock +++ b/requirements.lock @@ -25,7 +25,7 @@ h11==0.14.0 # via httpcore httpcore==1.0.2 # via httpx -httpx==0.25.2 +httpx==0.28.1 # via browserbase idna==3.4 # via anyio @@ -37,7 +37,6 @@ pydantic-core==2.27.1 sniffio==1.3.0 # via anyio # via browserbase - # via httpx typing-extensions==4.12.2 # via anyio # via browserbase From 3eacd326dfee314a88f3932f9cfd0cf09e36744a Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 8 Jan 2025 04:32:54 +0000 Subject: [PATCH 066/330] fix(client): only call .close() when needed (#97) --- src/browserbase/_base_client.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/browserbase/_base_client.py b/src/browserbase/_base_client.py index d8b28d9a..b21f4797 100644 --- a/src/browserbase/_base_client.py +++ b/src/browserbase/_base_client.py @@ -767,6 +767,9 @@ def __init__(self, **kwargs: Any) -> None: class SyncHttpxClientWrapper(DefaultHttpxClient): def __del__(self) -> None: + if self.is_closed: + return + try: self.close() except Exception: @@ -1334,6 +1337,9 @@ def __init__(self, **kwargs: Any) -> None: 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()) From b37855bebc77f554888594d6f587747b22b06177 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 9 Jan 2025 04:28:56 +0000 Subject: [PATCH 067/330] docs: fix typos (#98) --- README.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 95cb49b6..e55f84a0 100644 --- a/README.md +++ b/README.md @@ -107,7 +107,7 @@ except browserbase.APIStatusError as e: print(e.response) ``` -Error codes are as followed: +Error codes are as follows: | Status Code | Error Type | | ----------- | -------------------------- | @@ -246,8 +246,7 @@ If you need to access undocumented endpoints, params, or response properties, th #### 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) will be respected when making this -request. +http verbs. Options on the client will be respected (such as retries) when making this request. ```py import httpx From b5080dbf4def6f22d71fee4019b29f38b4e5ea09 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 9 Jan 2025 04:30:04 +0000 Subject: [PATCH 068/330] chore(internal): codegen related update (#99) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e55f84a0..42488fbd 100644 --- a/README.md +++ b/README.md @@ -318,7 +318,7 @@ with Browserbase() as client: 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)_. +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. From 84cc5222f81f27944751b3fe4cf45d4c2a922965 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 10 Jan 2025 04:11:19 +0000 Subject: [PATCH 069/330] fix: correctly handle deserialising `cls` fields (#100) --- src/browserbase/_models.py | 8 ++++---- tests/test_models.py | 10 ++++++++++ 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/src/browserbase/_models.py b/src/browserbase/_models.py index d56ea1d9..9a918aab 100644 --- a/src/browserbase/_models.py +++ b/src/browserbase/_models.py @@ -179,14 +179,14 @@ def __str__(self) -> str: @classmethod @override def construct( # pyright: ignore[reportIncompatibleMethodOverride] - cls: Type[ModelT], + __cls: Type[ModelT], _fields_set: set[str] | None = None, **values: object, ) -> ModelT: - m = cls.__new__(cls) + m = __cls.__new__(__cls) fields_values: dict[str, object] = {} - config = get_model_config(cls) + config = get_model_config(__cls) populate_by_name = ( config.allow_population_by_field_name if isinstance(config, _ConfigProtocol) @@ -196,7 +196,7 @@ def construct( # pyright: ignore[reportIncompatibleMethodOverride] if _fields_set is None: _fields_set = set() - model_fields = get_model_fields(cls) + 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): diff --git a/tests/test_models.py b/tests/test_models.py index 0db453cc..669d2190 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -844,3 +844,13 @@ class Model(BaseModel): assert m.alias == "foo" assert isinstance(m.union, str) assert m.union == "bar" + + +@pytest.mark.skipif(not PYDANTIC_V2, 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) From 4ae41411ff84d5a2dfc103bef3a4a95d1e4603fc Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 15 Jan 2025 01:16:54 +0000 Subject: [PATCH 070/330] feat(api): api update (#101) --- .stats.yml | 2 +- src/browserbase/resources/sessions/sessions.py | 8 ++++++++ src/browserbase/types/session.py | 3 +++ src/browserbase/types/session_create_params.py | 3 +++ src/browserbase/types/session_create_response.py | 3 +++ src/browserbase/types/sessions/session_log.py | 2 -- tests/api_resources/test_sessions.py | 2 ++ 7 files changed, 20 insertions(+), 3 deletions(-) diff --git a/.stats.yml b/.stats.yml index d42b050b..b76e3ecb 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,2 +1,2 @@ configured_endpoints: 18 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fbrowserbase-7f88912695bab2b98cb73137e6f36125d02fdfaf8eed4532ee1c82385609a259.yml +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fbrowserbase-3b940292f5146d4659546ef49685da0a2877a622957e2cf48c6bc2ccf3c153ca.yml diff --git a/src/browserbase/resources/sessions/sessions.py b/src/browserbase/resources/sessions/sessions.py index 15d8e700..513b6c22 100644 --- a/src/browserbase/resources/sessions/sessions.py +++ b/src/browserbase/resources/sessions/sessions.py @@ -108,6 +108,7 @@ def create( proxies: Union[bool, Iterable[session_create_params.ProxiesUnionMember1]] | NotGiven = NOT_GIVEN, region: Literal["us-west-2", "us-east-1", "eu-central-1", "ap-southeast-1"] | NotGiven = NOT_GIVEN, api_timeout: int | NotGiven = NOT_GIVEN, + user_metadata: object | NotGiven = NOT_GIVEN, # 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, @@ -137,6 +138,8 @@ def create( api_timeout: Duration in seconds after which the session will automatically end. Defaults to the Project's `defaultTimeout`. + user_metadata: Arbitrary user metadata to attach to the session. + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -156,6 +159,7 @@ def create( "proxies": proxies, "region": region, "api_timeout": api_timeout, + "user_metadata": user_metadata, }, session_create_params.SessionCreateParams, ), @@ -362,6 +366,7 @@ async def create( proxies: Union[bool, Iterable[session_create_params.ProxiesUnionMember1]] | NotGiven = NOT_GIVEN, region: Literal["us-west-2", "us-east-1", "eu-central-1", "ap-southeast-1"] | NotGiven = NOT_GIVEN, api_timeout: int | NotGiven = NOT_GIVEN, + user_metadata: object | NotGiven = NOT_GIVEN, # 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, @@ -391,6 +396,8 @@ async def create( api_timeout: Duration in seconds after which the session will automatically end. Defaults to the Project's `defaultTimeout`. + user_metadata: Arbitrary user metadata to attach to the session. + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -410,6 +417,7 @@ async def create( "proxies": proxies, "region": region, "api_timeout": api_timeout, + "user_metadata": user_metadata, }, session_create_params.SessionCreateParams, ), diff --git a/src/browserbase/types/session.py b/src/browserbase/types/session.py index 8bd47f93..eb9994e4 100644 --- a/src/browserbase/types/session.py +++ b/src/browserbase/types/session.py @@ -46,3 +46,6 @@ class Session(BaseModel): memory_usage: Optional[int] = FieldInfo(alias="memoryUsage", default=None) """Memory used by the Session""" + + user_metadata: Optional[object] = FieldInfo(alias="userMetadata", default=None) + """Arbitrary user metadata to attach to the session.""" diff --git a/src/browserbase/types/session_create_params.py b/src/browserbase/types/session_create_params.py index bd643b38..b026c9fb 100644 --- a/src/browserbase/types/session_create_params.py +++ b/src/browserbase/types/session_create_params.py @@ -57,6 +57,9 @@ class SessionCreateParams(TypedDict, total=False): Defaults to the Project's `defaultTimeout`. """ + user_metadata: Annotated[object, PropertyInfo(alias="userMetadata")] + """Arbitrary user metadata to attach to the session.""" + class BrowserSettingsContext(TypedDict, total=False): id: Required[str] diff --git a/src/browserbase/types/session_create_response.py b/src/browserbase/types/session_create_response.py index 8c9ae097..5bd94431 100644 --- a/src/browserbase/types/session_create_response.py +++ b/src/browserbase/types/session_create_response.py @@ -55,3 +55,6 @@ class SessionCreateResponse(BaseModel): memory_usage: Optional[int] = FieldInfo(alias="memoryUsage", default=None) """Memory used by the Session""" + + user_metadata: Optional[object] = FieldInfo(alias="userMetadata", default=None) + """Arbitrary user metadata to attach to the session.""" diff --git a/src/browserbase/types/sessions/session_log.py b/src/browserbase/types/sessions/session_log.py index d15eb831..af58030a 100644 --- a/src/browserbase/types/sessions/session_log.py +++ b/src/browserbase/types/sessions/session_log.py @@ -28,8 +28,6 @@ class Response(BaseModel): class SessionLog(BaseModel): - event_id: str = FieldInfo(alias="eventId") - method: str page_id: int = FieldInfo(alias="pageId") diff --git a/tests/api_resources/test_sessions.py b/tests/api_resources/test_sessions.py index 7b9fbce1..6378f27f 100644 --- a/tests/api_resources/test_sessions.py +++ b/tests/api_resources/test_sessions.py @@ -66,6 +66,7 @@ def test_method_create_with_all_params(self, client: Browserbase) -> None: proxies=True, region="us-west-2", api_timeout=60, + user_metadata={}, ) assert_matches_type(SessionCreateResponse, session, path=["response"]) @@ -295,6 +296,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncBrowserbas proxies=True, region="us-west-2", api_timeout=60, + user_metadata={}, ) assert_matches_type(SessionCreateResponse, session, path=["response"]) From 72866990ed1a5ed86e127f5936cf455170a1d634 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 17 Jan 2025 04:11:42 +0000 Subject: [PATCH 071/330] chore(internal): codegen related update (#102) --- mypy.ini | 2 +- requirements-dev.lock | 4 ++-- src/browserbase/_response.py | 8 +++++++- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/mypy.ini b/mypy.ini index 9e79a7c7..811af717 100644 --- a/mypy.ini +++ b/mypy.ini @@ -41,7 +41,7 @@ cache_fine_grained = True # ``` # Changing this codegen to make mypy happy would increase complexity # and would not be worth it. -disable_error_code = func-returns-value +disable_error_code = func-returns-value,overload-cannot-match # https://github.com/python/mypy/issues/12162 [mypy.overrides] diff --git a/requirements-dev.lock b/requirements-dev.lock index a9c9df5c..97fc565b 100644 --- a/requirements-dev.lock +++ b/requirements-dev.lock @@ -48,7 +48,7 @@ markdown-it-py==3.0.0 # via rich mdurl==0.1.2 # via markdown-it-py -mypy==1.13.0 +mypy==1.14.1 mypy-extensions==1.0.0 # via mypy nest-asyncio==1.6.0 @@ -68,7 +68,7 @@ pydantic-core==2.27.1 # via pydantic pygments==2.18.0 # via rich -pyright==1.1.390 +pyright==1.1.392.post0 pytest==8.3.3 # via pytest-asyncio pytest-asyncio==0.24.0 diff --git a/src/browserbase/_response.py b/src/browserbase/_response.py index 18768050..75c0fb98 100644 --- a/src/browserbase/_response.py +++ b/src/browserbase/_response.py @@ -210,7 +210,13 @@ def _parse(self, *, to: type[_T] | None = None) -> R | _T: raise ValueError(f"Subclasses of httpx.Response cannot be passed to `cast_to`") return cast(R, response) - if inspect.isclass(origin) and not issubclass(origin, BaseModel) and issubclass(origin, pydantic.BaseModel): + 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 browserbase import BaseModel`" ) From 75d622a35bc3638397d609913eb22b10b12321ac Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 17 Jan 2025 20:20:46 +0000 Subject: [PATCH 072/330] feat(api): api update (#104) --- .stats.yml | 2 +- src/browserbase/resources/sessions/sessions.py | 18 ++++++++++++++++-- src/browserbase/types/session_list_params.py | 2 ++ tests/api_resources/test_sessions.py | 2 ++ 4 files changed, 21 insertions(+), 3 deletions(-) diff --git a/.stats.yml b/.stats.yml index b76e3ecb..22ef4cf6 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,2 +1,2 @@ configured_endpoints: 18 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fbrowserbase-3b940292f5146d4659546ef49685da0a2877a622957e2cf48c6bc2ccf3c153ca.yml +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fbrowserbase-1e31d897af1fa5faba941e1170e9de8bbdbd169f84468a5554df02d807d2fa05.yml diff --git a/src/browserbase/resources/sessions/sessions.py b/src/browserbase/resources/sessions/sessions.py index 513b6c22..e28522a9 100644 --- a/src/browserbase/resources/sessions/sessions.py +++ b/src/browserbase/resources/sessions/sessions.py @@ -254,6 +254,7 @@ def update( def list( self, *, + q: str | NotGiven = NOT_GIVEN, status: Literal["RUNNING", "ERROR", "TIMED_OUT", "COMPLETED"] | NotGiven = NOT_GIVEN, # 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. @@ -281,7 +282,13 @@ def list( extra_query=extra_query, extra_body=extra_body, timeout=timeout, - query=maybe_transform({"status": status}, session_list_params.SessionListParams), + query=maybe_transform( + { + "q": q, + "status": status, + }, + session_list_params.SessionListParams, + ), ), cast_to=SessionListResponse, ) @@ -512,6 +519,7 @@ async def update( async def list( self, *, + q: str | NotGiven = NOT_GIVEN, status: Literal["RUNNING", "ERROR", "TIMED_OUT", "COMPLETED"] | NotGiven = NOT_GIVEN, # 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. @@ -539,7 +547,13 @@ async def list( extra_query=extra_query, extra_body=extra_body, timeout=timeout, - query=await async_maybe_transform({"status": status}, session_list_params.SessionListParams), + query=await async_maybe_transform( + { + "q": q, + "status": status, + }, + session_list_params.SessionListParams, + ), ), cast_to=SessionListResponse, ) diff --git a/src/browserbase/types/session_list_params.py b/src/browserbase/types/session_list_params.py index 7ba4798c..9a2c39c0 100644 --- a/src/browserbase/types/session_list_params.py +++ b/src/browserbase/types/session_list_params.py @@ -8,4 +8,6 @@ class SessionListParams(TypedDict, total=False): + q: str + status: Literal["RUNNING", "ERROR", "TIMED_OUT", "COMPLETED"] diff --git a/tests/api_resources/test_sessions.py b/tests/api_resources/test_sessions.py index 6378f27f..597090a7 100644 --- a/tests/api_resources/test_sessions.py +++ b/tests/api_resources/test_sessions.py @@ -186,6 +186,7 @@ def test_method_list(self, client: Browserbase) -> None: @parametrize def test_method_list_with_all_params(self, client: Browserbase) -> None: session = client.sessions.list( + q="q", status="RUNNING", ) assert_matches_type(SessionListResponse, session, path=["response"]) @@ -416,6 +417,7 @@ async def test_method_list(self, async_client: AsyncBrowserbase) -> None: @parametrize async def test_method_list_with_all_params(self, async_client: AsyncBrowserbase) -> None: session = await async_client.sessions.list( + q="q", status="RUNNING", ) assert_matches_type(SessionListResponse, session, path=["response"]) From e488d62517500cb0bf4e712c56f3ec95591e07ad Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 21 Jan 2025 17:04:36 +0000 Subject: [PATCH 073/330] feat(api): api update (#105) --- .stats.yml | 2 +- src/browserbase/types/session_create_params.py | 3 +++ tests/api_resources/test_sessions.py | 2 ++ 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index 22ef4cf6..842eb9a7 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,2 +1,2 @@ configured_endpoints: 18 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fbrowserbase-1e31d897af1fa5faba941e1170e9de8bbdbd169f84468a5554df02d807d2fa05.yml +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fbrowserbase-f1ba1f2c1512973c1640f7e2d27c72c4f5c49ec07e70b026d52818e7f8b1468e.yml diff --git a/src/browserbase/types/session_create_params.py b/src/browserbase/types/session_create_params.py index b026c9fb..d792889a 100644 --- a/src/browserbase/types/session_create_params.py +++ b/src/browserbase/types/session_create_params.py @@ -110,6 +110,9 @@ class BrowserSettingsViewport(TypedDict, total=False): class BrowserSettings(TypedDict, total=False): + advanced_stealth: Annotated[bool, PropertyInfo(alias="advancedStealth")] + """Advanced Browser Stealth Mode""" + block_ads: Annotated[bool, PropertyInfo(alias="blockAds")] """Enable or disable ad blocking in the browser. Defaults to `false`.""" diff --git a/tests/api_resources/test_sessions.py b/tests/api_resources/test_sessions.py index 597090a7..bdc52b90 100644 --- a/tests/api_resources/test_sessions.py +++ b/tests/api_resources/test_sessions.py @@ -34,6 +34,7 @@ def test_method_create_with_all_params(self, client: Browserbase) -> None: session = client.sessions.create( project_id="projectId", browser_settings={ + "advanced_stealth": True, "block_ads": True, "context": { "id": "id", @@ -265,6 +266,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncBrowserbas session = await async_client.sessions.create( project_id="projectId", browser_settings={ + "advanced_stealth": True, "block_ads": True, "context": { "id": "id", From a525d26598eb1d72e86d808999bb4aa8452fdd14 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 22 Jan 2025 04:23:33 +0000 Subject: [PATCH 074/330] chore(internal): codegen related update (#106) --- pyproject.toml | 1 + src/browserbase/resources/contexts.py | 4 +-- src/browserbase/resources/extensions.py | 4 +-- src/browserbase/resources/projects.py | 4 +-- .../resources/sessions/downloads.py | 4 +-- src/browserbase/resources/sessions/logs.py | 4 +-- .../resources/sessions/recording.py | 4 +-- .../resources/sessions/sessions.py | 4 +-- src/browserbase/resources/sessions/uploads.py | 4 +-- tests/test_client.py | 25 +++++++++++++------ 10 files changed, 35 insertions(+), 23 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index a1a1e201..076b77a0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -129,6 +129,7 @@ testpaths = ["tests"] addopts = "--tb=short" xfail_strict = true asyncio_mode = "auto" +asyncio_default_fixture_loop_scope = "session" filterwarnings = [ "error" ] diff --git a/src/browserbase/resources/contexts.py b/src/browserbase/resources/contexts.py index 806cb012..486cd5ff 100644 --- a/src/browserbase/resources/contexts.py +++ b/src/browserbase/resources/contexts.py @@ -30,7 +30,7 @@ class ContextsResource(SyncAPIResource): @cached_property def with_raw_response(self) -> ContextsResourceWithRawResponse: """ - This property can be used as a prefix for any HTTP method call to return the + 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/sdk-python#accessing-raw-response-data-eg-headers @@ -153,7 +153,7 @@ class AsyncContextsResource(AsyncAPIResource): @cached_property def with_raw_response(self) -> AsyncContextsResourceWithRawResponse: """ - This property can be used as a prefix for any HTTP method call to return the + 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/sdk-python#accessing-raw-response-data-eg-headers diff --git a/src/browserbase/resources/extensions.py b/src/browserbase/resources/extensions.py index dc6c0ac7..a98685a8 100644 --- a/src/browserbase/resources/extensions.py +++ b/src/browserbase/resources/extensions.py @@ -32,7 +32,7 @@ class ExtensionsResource(SyncAPIResource): @cached_property def with_raw_response(self) -> ExtensionsResourceWithRawResponse: """ - This property can be used as a prefix for any HTTP method call to return the + 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/sdk-python#accessing-raw-response-data-eg-headers @@ -159,7 +159,7 @@ class AsyncExtensionsResource(AsyncAPIResource): @cached_property def with_raw_response(self) -> AsyncExtensionsResourceWithRawResponse: """ - This property can be used as a prefix for any HTTP method call to return the + 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/sdk-python#accessing-raw-response-data-eg-headers diff --git a/src/browserbase/resources/projects.py b/src/browserbase/resources/projects.py index bf4a5df9..fb337a02 100644 --- a/src/browserbase/resources/projects.py +++ b/src/browserbase/resources/projects.py @@ -25,7 +25,7 @@ class ProjectsResource(SyncAPIResource): @cached_property def with_raw_response(self) -> ProjectsResourceWithRawResponse: """ - This property can be used as a prefix for any HTTP method call to return the + 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/sdk-python#accessing-raw-response-data-eg-headers @@ -131,7 +131,7 @@ class AsyncProjectsResource(AsyncAPIResource): @cached_property def with_raw_response(self) -> AsyncProjectsResourceWithRawResponse: """ - This property can be used as a prefix for any HTTP method call to return the + 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/sdk-python#accessing-raw-response-data-eg-headers diff --git a/src/browserbase/resources/sessions/downloads.py b/src/browserbase/resources/sessions/downloads.py index 461163b0..9ee49759 100644 --- a/src/browserbase/resources/sessions/downloads.py +++ b/src/browserbase/resources/sessions/downloads.py @@ -26,7 +26,7 @@ class DownloadsResource(SyncAPIResource): @cached_property def with_raw_response(self) -> DownloadsResourceWithRawResponse: """ - This property can be used as a prefix for any HTTP method call to return the + 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/sdk-python#accessing-raw-response-data-eg-headers @@ -81,7 +81,7 @@ class AsyncDownloadsResource(AsyncAPIResource): @cached_property def with_raw_response(self) -> AsyncDownloadsResourceWithRawResponse: """ - This property can be used as a prefix for any HTTP method call to return the + 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/sdk-python#accessing-raw-response-data-eg-headers diff --git a/src/browserbase/resources/sessions/logs.py b/src/browserbase/resources/sessions/logs.py index 07fb5818..2a42c9dc 100644 --- a/src/browserbase/resources/sessions/logs.py +++ b/src/browserbase/resources/sessions/logs.py @@ -23,7 +23,7 @@ class LogsResource(SyncAPIResource): @cached_property def with_raw_response(self) -> LogsResourceWithRawResponse: """ - This property can be used as a prefix for any HTTP method call to return the + 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/sdk-python#accessing-raw-response-data-eg-headers @@ -77,7 +77,7 @@ class AsyncLogsResource(AsyncAPIResource): @cached_property def with_raw_response(self) -> AsyncLogsResourceWithRawResponse: """ - This property can be used as a prefix for any HTTP method call to return the + 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/sdk-python#accessing-raw-response-data-eg-headers diff --git a/src/browserbase/resources/sessions/recording.py b/src/browserbase/resources/sessions/recording.py index b216fd9b..856b2927 100644 --- a/src/browserbase/resources/sessions/recording.py +++ b/src/browserbase/resources/sessions/recording.py @@ -23,7 +23,7 @@ class RecordingResource(SyncAPIResource): @cached_property def with_raw_response(self) -> RecordingResourceWithRawResponse: """ - This property can be used as a prefix for any HTTP method call to return the + 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/sdk-python#accessing-raw-response-data-eg-headers @@ -77,7 +77,7 @@ class AsyncRecordingResource(AsyncAPIResource): @cached_property def with_raw_response(self) -> AsyncRecordingResourceWithRawResponse: """ - This property can be used as a prefix for any HTTP method call to return the + 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/sdk-python#accessing-raw-response-data-eg-headers diff --git a/src/browserbase/resources/sessions/sessions.py b/src/browserbase/resources/sessions/sessions.py index e28522a9..33a94ca0 100644 --- a/src/browserbase/resources/sessions/sessions.py +++ b/src/browserbase/resources/sessions/sessions.py @@ -82,7 +82,7 @@ def uploads(self) -> UploadsResource: @cached_property def with_raw_response(self) -> SessionsResourceWithRawResponse: """ - This property can be used as a prefix for any HTTP method call to return the + 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/sdk-python#accessing-raw-response-data-eg-headers @@ -347,7 +347,7 @@ def uploads(self) -> AsyncUploadsResource: @cached_property def with_raw_response(self) -> AsyncSessionsResourceWithRawResponse: """ - This property can be used as a prefix for any HTTP method call to return the + 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/sdk-python#accessing-raw-response-data-eg-headers diff --git a/src/browserbase/resources/sessions/uploads.py b/src/browserbase/resources/sessions/uploads.py index e985e4d9..eed93499 100644 --- a/src/browserbase/resources/sessions/uploads.py +++ b/src/browserbase/resources/sessions/uploads.py @@ -32,7 +32,7 @@ class UploadsResource(SyncAPIResource): @cached_property def with_raw_response(self) -> UploadsResourceWithRawResponse: """ - This property can be used as a prefix for any HTTP method call to return the + 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/sdk-python#accessing-raw-response-data-eg-headers @@ -95,7 +95,7 @@ class AsyncUploadsResource(AsyncAPIResource): @cached_property def with_raw_response(self) -> AsyncUploadsResourceWithRawResponse: """ - This property can be used as a prefix for any HTTP method call to return the + 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/sdk-python#accessing-raw-response-data-eg-headers diff --git a/tests/test_client.py b/tests/test_client.py index 776b48b5..20ac94b0 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -6,6 +6,7 @@ import os import sys import json +import time import asyncio import inspect import subprocess @@ -1639,10 +1640,20 @@ async def test_main() -> None: [sys.executable, "-c", test_code], text=True, ) as process: - try: - process.wait(2) - if process.returncode: - raise AssertionError("calling get_platform using asyncify resulted in a non-zero exit code") - except subprocess.TimeoutExpired as e: - process.kill() - raise AssertionError("calling get_platform using asyncify resulted in a hung process") from e + timeout = 10 # seconds + + start_time = time.monotonic() + while True: + return_code = process.poll() + if return_code is not None: + if return_code != 0: + raise AssertionError("calling get_platform using asyncify resulted in a non-zero exit code") + + # success + break + + if time.monotonic() - start_time > timeout: + process.kill() + raise AssertionError("calling get_platform using asyncify resulted in a hung process") + + time.sleep(0.1) From dbc2c4a83a7679d9ee719a7e27c3dac68889ba6b Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 22 Jan 2025 04:37:54 +0000 Subject: [PATCH 075/330] chore(internal): codegen related update (#107) --- src/browserbase/_response.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/browserbase/_response.py b/src/browserbase/_response.py index 75c0fb98..e79cb15f 100644 --- a/src/browserbase/_response.py +++ b/src/browserbase/_response.py @@ -136,6 +136,8 @@ def _parse(self, *, to: type[_T] | None = None) -> R | _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): @@ -195,8 +197,6 @@ def _parse(self, *, to: type[_T] | None = None) -> R | _T: if cast_to == bool: return cast(R, response.text.lower() == "true") - origin = get_origin(cast_to) or cast_to - if origin == APIResponse: raise RuntimeError("Unexpected state - cast_to is `APIResponse`") From d75d698c47c8770923f05042ef96194238c44edc Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 23 Jan 2025 17:27:01 +0000 Subject: [PATCH 076/330] feat(api): api update (#109) --- .stats.yml | 2 +- .../resources/sessions/sessions.py | 30 +++++++++++++------ src/browserbase/types/session.py | 10 +++++-- .../types/session_create_params.py | 10 +++++-- .../types/session_create_response.py | 10 +++++-- src/browserbase/types/session_list_params.py | 6 ++++ tests/api_resources/test_sessions.py | 4 +-- 7 files changed, 51 insertions(+), 21 deletions(-) diff --git a/.stats.yml b/.stats.yml index 842eb9a7..9a2cf9d0 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,2 +1,2 @@ configured_endpoints: 18 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fbrowserbase-f1ba1f2c1512973c1640f7e2d27c72c4f5c49ec07e70b026d52818e7f8b1468e.yml +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fbrowserbase-6a5cbe2f816042d594335d77f9600cd47cdb9c21d9d60971a2eca87983061c72.yml diff --git a/src/browserbase/resources/sessions/sessions.py b/src/browserbase/resources/sessions/sessions.py index 33a94ca0..0572d913 100644 --- a/src/browserbase/resources/sessions/sessions.py +++ b/src/browserbase/resources/sessions/sessions.py @@ -2,7 +2,7 @@ from __future__ import annotations -from typing import Union, Iterable +from typing import Dict, Union, Iterable from typing_extensions import Literal import httpx @@ -108,7 +108,7 @@ def create( proxies: Union[bool, Iterable[session_create_params.ProxiesUnionMember1]] | NotGiven = NOT_GIVEN, region: Literal["us-west-2", "us-east-1", "eu-central-1", "ap-southeast-1"] | NotGiven = NOT_GIVEN, api_timeout: int | NotGiven = NOT_GIVEN, - user_metadata: object | NotGiven = NOT_GIVEN, + user_metadata: Dict[str, object] | NotGiven = NOT_GIVEN, # 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, @@ -138,7 +138,8 @@ def create( api_timeout: Duration in seconds after which the session will automatically end. Defaults to the Project's `defaultTimeout`. - user_metadata: Arbitrary user metadata to attach to the session. + user_metadata: Arbitrary user metadata to attach to the session. To learn more about user + metadata, see [User Metadata](/features/sessions#user-metadata). extra_headers: Send extra headers @@ -263,10 +264,15 @@ def list( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> SessionListResponse: - """ - List Sessions + """List Sessions Args: + q: Query sessions by user metadata. + + See + [Querying Sessions by User Metadata](/features/sessions#querying-sessions-by-user-metadata) + for the schema of this query. + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -373,7 +379,7 @@ async def create( proxies: Union[bool, Iterable[session_create_params.ProxiesUnionMember1]] | NotGiven = NOT_GIVEN, region: Literal["us-west-2", "us-east-1", "eu-central-1", "ap-southeast-1"] | NotGiven = NOT_GIVEN, api_timeout: int | NotGiven = NOT_GIVEN, - user_metadata: object | NotGiven = NOT_GIVEN, + user_metadata: Dict[str, object] | NotGiven = NOT_GIVEN, # 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, @@ -403,7 +409,8 @@ async def create( api_timeout: Duration in seconds after which the session will automatically end. Defaults to the Project's `defaultTimeout`. - user_metadata: Arbitrary user metadata to attach to the session. + user_metadata: Arbitrary user metadata to attach to the session. To learn more about user + metadata, see [User Metadata](/features/sessions#user-metadata). extra_headers: Send extra headers @@ -528,10 +535,15 @@ async def list( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> SessionListResponse: - """ - List Sessions + """List Sessions Args: + q: Query sessions by user metadata. + + See + [Querying Sessions by User Metadata](/features/sessions#querying-sessions-by-user-metadata) + for the schema of this query. + extra_headers: Send extra headers extra_query: Add additional query parameters to the request diff --git a/src/browserbase/types/session.py b/src/browserbase/types/session.py index eb9994e4..16450e29 100644 --- a/src/browserbase/types/session.py +++ b/src/browserbase/types/session.py @@ -1,6 +1,6 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -from typing import Optional +from typing import Dict, Optional from datetime import datetime from typing_extensions import Literal @@ -47,5 +47,9 @@ class Session(BaseModel): memory_usage: Optional[int] = FieldInfo(alias="memoryUsage", default=None) """Memory used by the Session""" - user_metadata: Optional[object] = FieldInfo(alias="userMetadata", default=None) - """Arbitrary user metadata to attach to the session.""" + user_metadata: Optional[Dict[str, object]] = FieldInfo(alias="userMetadata", default=None) + """Arbitrary user metadata to attach to the session. + + To learn more about user metadata, see + [User Metadata](/features/sessions#user-metadata). + """ diff --git a/src/browserbase/types/session_create_params.py b/src/browserbase/types/session_create_params.py index d792889a..5e76037d 100644 --- a/src/browserbase/types/session_create_params.py +++ b/src/browserbase/types/session_create_params.py @@ -2,7 +2,7 @@ from __future__ import annotations -from typing import List, Union, Iterable +from typing import Dict, List, Union, Iterable from typing_extensions import Literal, Required, Annotated, TypeAlias, TypedDict from .._utils import PropertyInfo @@ -57,8 +57,12 @@ class SessionCreateParams(TypedDict, total=False): Defaults to the Project's `defaultTimeout`. """ - user_metadata: Annotated[object, PropertyInfo(alias="userMetadata")] - """Arbitrary user metadata to attach to the session.""" + user_metadata: Annotated[Dict[str, object], PropertyInfo(alias="userMetadata")] + """Arbitrary user metadata to attach to the session. + + To learn more about user metadata, see + [User Metadata](/features/sessions#user-metadata). + """ class BrowserSettingsContext(TypedDict, total=False): diff --git a/src/browserbase/types/session_create_response.py b/src/browserbase/types/session_create_response.py index 5bd94431..b548d50f 100644 --- a/src/browserbase/types/session_create_response.py +++ b/src/browserbase/types/session_create_response.py @@ -1,6 +1,6 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -from typing import Optional +from typing import Dict, Optional from datetime import datetime from typing_extensions import Literal @@ -56,5 +56,9 @@ class SessionCreateResponse(BaseModel): memory_usage: Optional[int] = FieldInfo(alias="memoryUsage", default=None) """Memory used by the Session""" - user_metadata: Optional[object] = FieldInfo(alias="userMetadata", default=None) - """Arbitrary user metadata to attach to the session.""" + user_metadata: Optional[Dict[str, object]] = FieldInfo(alias="userMetadata", default=None) + """Arbitrary user metadata to attach to the session. + + To learn more about user metadata, see + [User Metadata](/features/sessions#user-metadata). + """ diff --git a/src/browserbase/types/session_list_params.py b/src/browserbase/types/session_list_params.py index 9a2c39c0..54b0a05c 100644 --- a/src/browserbase/types/session_list_params.py +++ b/src/browserbase/types/session_list_params.py @@ -9,5 +9,11 @@ class SessionListParams(TypedDict, total=False): q: str + """Query sessions by user metadata. + + See + [Querying Sessions by User Metadata](/features/sessions#querying-sessions-by-user-metadata) + for the schema of this query. + """ status: Literal["RUNNING", "ERROR", "TIMED_OUT", "COMPLETED"] diff --git a/tests/api_resources/test_sessions.py b/tests/api_resources/test_sessions.py index bdc52b90..482624f8 100644 --- a/tests/api_resources/test_sessions.py +++ b/tests/api_resources/test_sessions.py @@ -67,7 +67,7 @@ def test_method_create_with_all_params(self, client: Browserbase) -> None: proxies=True, region="us-west-2", api_timeout=60, - user_metadata={}, + user_metadata={"foo": "bar"}, ) assert_matches_type(SessionCreateResponse, session, path=["response"]) @@ -299,7 +299,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncBrowserbas proxies=True, region="us-west-2", api_timeout=60, - user_metadata={}, + user_metadata={"foo": "bar"}, ) assert_matches_type(SessionCreateResponse, session, path=["response"]) From c95fefef0258f0eb517810ee45040001595b2a57 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 24 Jan 2025 03:23:53 +0000 Subject: [PATCH 077/330] chore(internal): minor formatting changes (#110) --- .github/workflows/ci.yml | 3 +-- scripts/bootstrap | 2 +- scripts/lint | 1 - 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 40293964..c8a8a4f7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,7 +12,6 @@ jobs: lint: name: lint runs-on: ubuntu-latest - steps: - uses: actions/checkout@v4 @@ -30,6 +29,7 @@ jobs: - name: Run lints run: ./scripts/lint + test: name: test runs-on: ubuntu-latest @@ -50,4 +50,3 @@ jobs: - name: Run tests run: ./scripts/test - diff --git a/scripts/bootstrap b/scripts/bootstrap index 8c5c60eb..e84fe62c 100755 --- a/scripts/bootstrap +++ b/scripts/bootstrap @@ -4,7 +4,7 @@ set -e cd "$(dirname "$0")/.." -if [ -f "Brewfile" ] && [ "$(uname -s)" = "Darwin" ]; then +if ! command -v rye >/dev/null 2>&1 && [ -f "Brewfile" ] && [ "$(uname -s)" = "Darwin" ]; then brew bundle check >/dev/null 2>&1 || { echo "==> Installing Homebrew dependencies…" brew bundle diff --git a/scripts/lint b/scripts/lint index a74a1988..feccbdde 100755 --- a/scripts/lint +++ b/scripts/lint @@ -9,4 +9,3 @@ rye run lint echo "==> Making sure it imports" rye run python -c 'import browserbase' - From 343fda475f87e535591ed988c0dc27132f87bc3d Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 28 Jan 2025 05:08:40 +0000 Subject: [PATCH 078/330] feat(api): api update (#111) --- .stats.yml | 2 +- src/browserbase/types/sessions/session_log.py | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.stats.yml b/.stats.yml index 9a2cf9d0..be077766 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,2 +1,2 @@ configured_endpoints: 18 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fbrowserbase-6a5cbe2f816042d594335d77f9600cd47cdb9c21d9d60971a2eca87983061c72.yml +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fbrowserbase-396a2b9092f645c5a9e46a1f3be8c2e45ca9ae079e1d39761eb0a73f56e24b15.yml diff --git a/src/browserbase/types/sessions/session_log.py b/src/browserbase/types/sessions/session_log.py index af58030a..428f518a 100644 --- a/src/browserbase/types/sessions/session_log.py +++ b/src/browserbase/types/sessions/session_log.py @@ -14,7 +14,7 @@ class Request(BaseModel): raw_body: str = FieldInfo(alias="rawBody") - timestamp: int + timestamp: Optional[int] = None """milliseconds that have elapsed since the UNIX epoch""" @@ -23,7 +23,7 @@ class Response(BaseModel): result: Dict[str, object] - timestamp: int + timestamp: Optional[int] = None """milliseconds that have elapsed since the UNIX epoch""" @@ -34,9 +34,6 @@ class SessionLog(BaseModel): session_id: str = FieldInfo(alias="sessionId") - timestamp: int - """milliseconds that have elapsed since the UNIX epoch""" - frame_id: Optional[str] = FieldInfo(alias="frameId", default=None) loader_id: Optional[str] = FieldInfo(alias="loaderId", default=None) @@ -44,3 +41,6 @@ class SessionLog(BaseModel): request: Optional[Request] = None response: Optional[Response] = None + + timestamp: Optional[int] = None + """milliseconds that have elapsed since the UNIX epoch""" From a0a8baad8dc5628ba93e290c807ea9417d96a824 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 28 Jan 2025 18:35:23 +0000 Subject: [PATCH 079/330] chore(internal): version bump (#112) --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/browserbase/_version.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 1214610a..2601677b 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "1.0.5" + ".": "1.1.0" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 076b77a0..9026d58c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "browserbase" -version = "1.0.5" +version = "1.1.0" description = "The official Python library for the Browserbase API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/browserbase/_version.py b/src/browserbase/_version.py index c871d01e..9621169e 100644 --- a/src/browserbase/_version.py +++ b/src/browserbase/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "browserbase" -__version__ = "1.0.5" # x-release-please-version +__version__ = "1.1.0" # x-release-please-version From 644c05b249ff16a9d150c4725cd77f2697d0a486 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 4 Feb 2025 03:05:29 +0000 Subject: [PATCH 080/330] chore(internal): change default timeout to an int (#113) --- src/browserbase/_constants.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/browserbase/_constants.py b/src/browserbase/_constants.py index a2ac3b6f..6ddf2c71 100644 --- a/src/browserbase/_constants.py +++ b/src/browserbase/_constants.py @@ -6,7 +6,7 @@ OVERRIDE_CAST_TO_HEADER = "____stainless_override_cast_to" # default timeout is 1 minute -DEFAULT_TIMEOUT = httpx.Timeout(timeout=60.0, connect=5.0) +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) From 3b41e6cf7fc1175e225ab232c2979b5ece53d40f Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 4 Feb 2025 03:07:41 +0000 Subject: [PATCH 081/330] chore(internal): bummp ruff dependency (#115) --- pyproject.toml | 2 +- requirements-dev.lock | 2 +- scripts/utils/ruffen-docs.py | 4 ++-- src/browserbase/_models.py | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 9026d58c..65d7bd09 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -177,7 +177,7 @@ select = [ "T201", "T203", # misuse of typing.TYPE_CHECKING - "TCH004", + "TC004", # import rules "TID251", ] diff --git a/requirements-dev.lock b/requirements-dev.lock index 97fc565b..2ad2c995 100644 --- a/requirements-dev.lock +++ b/requirements-dev.lock @@ -78,7 +78,7 @@ pytz==2023.3.post1 # via dirty-equals respx==0.22.0 rich==13.7.1 -ruff==0.6.9 +ruff==0.9.4 setuptools==68.2.2 # via nodeenv six==1.16.0 diff --git a/scripts/utils/ruffen-docs.py b/scripts/utils/ruffen-docs.py index 37b3d94f..0cf2bd2f 100644 --- a/scripts/utils/ruffen-docs.py +++ b/scripts/utils/ruffen-docs.py @@ -47,7 +47,7 @@ def _md_match(match: Match[str]) -> str: with _collect_error(match): code = format_code_block(code) code = textwrap.indent(code, match["indent"]) - return f'{match["before"]}{code}{match["after"]}' + return f"{match['before']}{code}{match['after']}" def _pycon_match(match: Match[str]) -> str: code = "" @@ -97,7 +97,7 @@ def finish_fragment() -> None: 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"]}' + return f"{match['before']}{code}{match['after']}" src = MD_RE.sub(_md_match, src) src = MD_PYCON_RE.sub(_md_pycon_match, src) diff --git a/src/browserbase/_models.py b/src/browserbase/_models.py index 9a918aab..12c34b7d 100644 --- a/src/browserbase/_models.py +++ b/src/browserbase/_models.py @@ -172,7 +172,7 @@ def to_json( @override def __str__(self) -> str: # mypy complains about an invalid self arg - return f'{self.__repr_name__()}({self.__repr_str__(", ")})' # type: ignore[misc] + 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. From 65ca69b8725f49ba8cafbf39ec31f25b2993773e Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 6 Feb 2025 03:13:14 +0000 Subject: [PATCH 082/330] feat(client): send `X-Stainless-Read-Timeout` header (#117) --- src/browserbase/_base_client.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/browserbase/_base_client.py b/src/browserbase/_base_client.py index b21f4797..0e3d4ae3 100644 --- a/src/browserbase/_base_client.py +++ b/src/browserbase/_base_client.py @@ -418,10 +418,17 @@ def _build_headers(self, options: FinalRequestOptions, *, retries_taken: int = 0 if idempotency_header and options.method.lower() != "get" and idempotency_header not in headers: headers[idempotency_header] = options.idempotency_key or self._idempotency_key() - # Don't set the retry count header if it was already set or removed by the caller. We check + # 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. - if "x-stainless-retry-count" not in (header.lower() for header in custom_headers): + 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 From 98f79df131b0e10820b33f9a384bd44c99d21a66 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 7 Feb 2025 03:11:17 +0000 Subject: [PATCH 083/330] chore(internal): fix type traversing dictionary params (#118) --- src/browserbase/_utils/_transform.py | 12 +++++++++++- tests/test_transform.py | 11 ++++++++++- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/src/browserbase/_utils/_transform.py b/src/browserbase/_utils/_transform.py index a6b62cad..18afd9d8 100644 --- a/src/browserbase/_utils/_transform.py +++ b/src/browserbase/_utils/_transform.py @@ -25,7 +25,7 @@ is_annotated_type, strip_annotated_type, ) -from .._compat import model_dump, is_typeddict +from .._compat import get_origin, model_dump, is_typeddict _T = TypeVar("_T") @@ -164,9 +164,14 @@ def _transform_recursive( 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)) @@ -307,9 +312,14 @@ async def _async_transform_recursive( 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)) diff --git a/tests/test_transform.py b/tests/test_transform.py index 03c2ecd4..32c44ae7 100644 --- a/tests/test_transform.py +++ b/tests/test_transform.py @@ -2,7 +2,7 @@ import io import pathlib -from typing import Any, List, Union, TypeVar, Iterable, Optional, cast +from typing import Any, Dict, List, Union, TypeVar, Iterable, Optional, cast from datetime import date, datetime from typing_extensions import Required, Annotated, TypedDict @@ -388,6 +388,15 @@ def my_iter() -> Iterable[Baz8]: } +@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")] From df03848e869639e91a834abf13fd05b39b6236ff Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 7 Feb 2025 03:13:53 +0000 Subject: [PATCH 084/330] chore(internal): minor type handling changes (#119) --- src/browserbase/_models.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/browserbase/_models.py b/src/browserbase/_models.py index 12c34b7d..c4401ff8 100644 --- a/src/browserbase/_models.py +++ b/src/browserbase/_models.py @@ -426,10 +426,16 @@ def construct_type(*, value: object, type_: object) -> object: 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` @@ -446,7 +452,7 @@ def construct_type(*, value: object, type_: object) -> object: if is_union(origin): try: - return validate_type(type_=cast("type[object]", type_), value=value) + return validate_type(type_=cast("type[object]", original_type or type_), value=value) except Exception: pass From e696c388821ef1b4da87f2e930e0ea0d3081c970 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 11 Feb 2025 22:57:00 +0000 Subject: [PATCH 085/330] chore(internal): version bump (#120) --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/browserbase/_version.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 2601677b..d0ab6645 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "1.1.0" + ".": "1.2.0" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 65d7bd09..69e53d56 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "browserbase" -version = "1.1.0" +version = "1.2.0" description = "The official Python library for the Browserbase API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/browserbase/_version.py b/src/browserbase/_version.py index 9621169e..4207df36 100644 --- a/src/browserbase/_version.py +++ b/src/browserbase/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "browserbase" -__version__ = "1.1.0" # x-release-please-version +__version__ = "1.2.0" # x-release-please-version From 3bdc6cfc2fc89874279e71e05a5c8812437099c0 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 13 Feb 2025 03:21:41 +0000 Subject: [PATCH 086/330] chore(internal): update client tests (#121) --- tests/test_client.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/tests/test_client.py b/tests/test_client.py index 20ac94b0..1c703f0b 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -23,6 +23,7 @@ from browserbase import Browserbase, AsyncBrowserbase, APIResponseValidationError from browserbase._types import Omit +from browserbase._utils import maybe_transform from browserbase._models import BaseModel, FinalRequestOptions from browserbase._constants import RAW_RESPONSE_HEADER from browserbase._exceptions import APIStatusError, APITimeoutError, BrowserbaseError, APIResponseValidationError @@ -32,6 +33,7 @@ BaseClient, make_request_options, ) +from browserbase.types.session_create_params import SessionCreateParams from .utils import update_env @@ -727,7 +729,7 @@ def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter) -> No with pytest.raises(APITimeoutError): self.client.post( "/v1/sessions", - body=cast(object, dict(project_id="your_project_id")), + body=cast(object, maybe_transform(dict(project_id="your_project_id"), SessionCreateParams)), cast_to=httpx.Response, options={"headers": {RAW_RESPONSE_HEADER: "stream"}}, ) @@ -742,7 +744,7 @@ def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter) -> Non with pytest.raises(APIStatusError): self.client.post( "/v1/sessions", - body=cast(object, dict(project_id="your_project_id")), + body=cast(object, maybe_transform(dict(project_id="your_project_id"), SessionCreateParams)), cast_to=httpx.Response, options={"headers": {RAW_RESPONSE_HEADER: "stream"}}, ) @@ -1507,7 +1509,7 @@ async def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter) with pytest.raises(APITimeoutError): await self.client.post( "/v1/sessions", - body=cast(object, dict(project_id="your_project_id")), + body=cast(object, maybe_transform(dict(project_id="your_project_id"), SessionCreateParams)), cast_to=httpx.Response, options={"headers": {RAW_RESPONSE_HEADER: "stream"}}, ) @@ -1522,7 +1524,7 @@ async def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter) with pytest.raises(APIStatusError): await self.client.post( "/v1/sessions", - body=cast(object, dict(project_id="your_project_id")), + body=cast(object, maybe_transform(dict(project_id="your_project_id"), SessionCreateParams)), cast_to=httpx.Response, options={"headers": {RAW_RESPONSE_HEADER: "stream"}}, ) From a88062ca060af47da95eb5d818306a5d1d858def Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 14 Feb 2025 03:13:24 +0000 Subject: [PATCH 087/330] fix: asyncify on non-asyncio runtimes (#123) --- src/browserbase/_utils/_sync.py | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/src/browserbase/_utils/_sync.py b/src/browserbase/_utils/_sync.py index 8b3aaf2b..ad7ec71b 100644 --- a/src/browserbase/_utils/_sync.py +++ b/src/browserbase/_utils/_sync.py @@ -7,16 +7,20 @@ from typing import Any, 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") if sys.version_info >= (3, 9): - to_thread = asyncio.to_thread + _asyncio_to_thread = asyncio.to_thread else: # backport of https://docs.python.org/3/library/asyncio-task.html#asyncio.to_thread # for Python 3.8 support - async def to_thread( + async def _asyncio_to_thread( func: Callable[T_ParamSpec, T_Retval], /, *args: T_ParamSpec.args, **kwargs: T_ParamSpec.kwargs ) -> Any: """Asynchronously run function *func* in a separate thread. @@ -34,6 +38,17 @@ async def to_thread( return await loop.run_in_executor(None, func_call) +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]]: """ From f3f02a3325d8ef8094aabe74d27e325153401eff Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 19 Feb 2025 03:23:43 +0000 Subject: [PATCH 088/330] chore(internal): codegen related update (#124) --- README.md | 17 +++++++++++++++++ src/browserbase/_files.py | 2 +- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 42488fbd..14ebbb10 100644 --- a/README.md +++ b/README.md @@ -77,6 +77,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`. +## File uploads + +Request parameters that correspond to file uploads can be passed as `bytes`, a [`PathLike`](https://docs.python.org/3/library/os.html#os.PathLike) instance or a tuple of `(filename, contents, media type)`. + +```python +from pathlib import Path +from browserbase import Browserbase + +client = Browserbase() + +client.extensions.create( + file=Path("/path/to/file"), +) +``` + +The async client uses the exact same interface. If you pass a [`PathLike`](https://docs.python.org/3/library/os.html#os.PathLike) instance, the file contents will be read asynchronously automatically. + ## 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 `browserbase.APIConnectionError` is raised. diff --git a/src/browserbase/_files.py b/src/browserbase/_files.py index 715cc207..c690226c 100644 --- a/src/browserbase/_files.py +++ b/src/browserbase/_files.py @@ -34,7 +34,7 @@ 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." + f"{prefix} to be bytes, an io.IOBase instance, PathLike or a tuple but received {type(obj)} instead. See https://github.com/browserbase/sdk-python/tree/main#file-uploads" ) from None From 512860d47c2e4ffd2932c2b3540bedc91ca7469b Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 21 Feb 2025 04:03:19 +0000 Subject: [PATCH 089/330] feat(client): allow passing `NotGiven` for body (#125) fix(client): mark some request bodies as optional --- src/browserbase/_base_client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/browserbase/_base_client.py b/src/browserbase/_base_client.py index 0e3d4ae3..2bdf8f74 100644 --- a/src/browserbase/_base_client.py +++ b/src/browserbase/_base_client.py @@ -518,7 +518,7 @@ def _build_request( # 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, - json=json_data, + json=json_data if is_given(json_data) else None, files=files, **kwargs, ) From fcd137a24e2a47a4807ab6945225224568403c34 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 22 Feb 2025 03:23:18 +0000 Subject: [PATCH 090/330] chore(internal): fix devcontainers setup (#126) --- .devcontainer/Dockerfile | 2 +- .devcontainer/devcontainer.json | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index ac9a2e75..55d20255 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -6,4 +6,4 @@ USER vscode RUN curl -sSf https://rye.astral.sh/get | RYE_VERSION="0.35.0" RYE_INSTALL_OPTION="--yes" bash ENV PATH=/home/vscode/.rye/shims:$PATH -RUN echo "[[ -d .venv ]] && source .venv/bin/activate" >> /home/vscode/.bashrc +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 bbeb30b1..c17fdc16 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -24,6 +24,9 @@ } } } + }, + "features": { + "ghcr.io/devcontainers/features/node:1": {} } // Features to add to the dev container. More info: https://containers.dev/features. From a8ec4c9ec41f447f4d3e7264562fa69efe66e3f1 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 26 Feb 2025 03:10:50 +0000 Subject: [PATCH 091/330] chore(internal): properly set __pydantic_private__ (#127) --- src/browserbase/_base_client.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/browserbase/_base_client.py b/src/browserbase/_base_client.py index 2bdf8f74..1955a7ac 100644 --- a/src/browserbase/_base_client.py +++ b/src/browserbase/_base_client.py @@ -63,7 +63,7 @@ ModelBuilderProtocol, ) from ._utils import is_dict, is_list, asyncify, is_given, lru_cache, is_mapping -from ._compat import model_copy, model_dump +from ._compat import PYDANTIC_V2, model_copy, model_dump from ._models import GenericModel, FinalRequestOptions, validate_type, construct_type from ._response import ( APIResponse, @@ -207,6 +207,9 @@ def _set_private_attributes( model: Type[_T], options: FinalRequestOptions, ) -> None: + if PYDANTIC_V2 and getattr(self, "__pydantic_private__", None) is None: + self.__pydantic_private__ = {} + self._model = model self._client = client self._options = options @@ -292,6 +295,9 @@ def _set_private_attributes( client: AsyncAPIClient, options: FinalRequestOptions, ) -> None: + if PYDANTIC_V2 and getattr(self, "__pydantic_private__", None) is None: + self.__pydantic_private__ = {} + self._model = model self._client = client self._options = options From 43a94ca0220ca421ac3783f06014fe7d4cd396d7 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 28 Feb 2025 03:02:23 +0000 Subject: [PATCH 092/330] docs: update URLs from stainlessapi.com to stainless.com (#128) More details at https://www.stainless.com/changelog/stainless-com --- README.md | 2 +- SECURITY.md | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 14ebbb10..75d6311c 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ The Browserbase Python library provides convenient access to the Browserbase RES 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.stainlessapi.com/). +It is generated with [Stainless](https://www.stainless.com/). ## Documentation diff --git a/SECURITY.md b/SECURITY.md index 4fdede87..e10eb19a 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -2,9 +2,9 @@ ## Reporting Security Issues -This SDK is generated by [Stainless Software Inc](http://stainlessapi.com). Stainless takes security seriously, and encourages you to report any security vulnerability promptly so that appropriate action can be taken. +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@stainlessapi.com. +To report a security issue, please contact the Stainless team at security@stainless.com. ## Responsible Disclosure From 393fce06a9281805c58b75b400ab640f2c8cad3c Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 28 Feb 2025 03:03:16 +0000 Subject: [PATCH 093/330] chore(docs): update client docstring (#129) --- src/browserbase/_client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/browserbase/_client.py b/src/browserbase/_client.py index b845ea86..15fde864 100644 --- a/src/browserbase/_client.py +++ b/src/browserbase/_client.py @@ -254,7 +254,7 @@ def __init__( # part of our public interface in the future. _strict_response_validation: bool = False, ) -> None: - """Construct a new async Browserbase client instance. + """Construct a new async AsyncBrowserbase client instance. This automatically infers the `api_key` argument from the `BROWSERBASE_API_KEY` environment variable if it is not provided. """ From 3f876053bb10fe7a0dfb91fe7feebb2cbcb2c9b4 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 4 Mar 2025 03:29:58 +0000 Subject: [PATCH 094/330] chore(internal): remove unused http client options forwarding (#130) --- src/browserbase/_base_client.py | 97 +-------------------------------- 1 file changed, 1 insertion(+), 96 deletions(-) diff --git a/src/browserbase/_base_client.py b/src/browserbase/_base_client.py index 1955a7ac..2956496d 100644 --- a/src/browserbase/_base_client.py +++ b/src/browserbase/_base_client.py @@ -9,7 +9,6 @@ import inspect import logging import platform -import warnings import email.utils from types import TracebackType from random import random @@ -36,7 +35,7 @@ import httpx import distro import pydantic -from httpx import URL, Limits +from httpx import URL from pydantic import PrivateAttr from . import _exceptions @@ -51,13 +50,10 @@ Timeout, NotGiven, ResponseT, - Transport, AnyMapping, PostParser, - ProxiesTypes, RequestFiles, HttpxSendArgs, - AsyncTransport, RequestOptions, HttpxRequestFiles, ModelBuilderProtocol, @@ -337,9 +333,6 @@ class BaseClient(Generic[_HttpxClientT, _DefaultStreamT]): _base_url: URL max_retries: int timeout: Union[float, Timeout, None] - _limits: httpx.Limits - _proxies: ProxiesTypes | None - _transport: Transport | AsyncTransport | None _strict_response_validation: bool _idempotency_header: str | None _default_stream_cls: type[_DefaultStreamT] | None = None @@ -352,9 +345,6 @@ def __init__( _strict_response_validation: bool, max_retries: int = DEFAULT_MAX_RETRIES, timeout: float | Timeout | None = DEFAULT_TIMEOUT, - limits: httpx.Limits, - transport: Transport | AsyncTransport | None, - proxies: ProxiesTypes | None, custom_headers: Mapping[str, str] | None = None, custom_query: Mapping[str, object] | None = None, ) -> None: @@ -362,9 +352,6 @@ def __init__( self._base_url = self._enforce_trailing_slash(URL(base_url)) self.max_retries = max_retries self.timeout = timeout - self._limits = limits - self._proxies = proxies - self._transport = transport self._custom_headers = custom_headers or {} self._custom_query = custom_query or {} self._strict_response_validation = _strict_response_validation @@ -800,46 +787,11 @@ def __init__( base_url: str | URL, max_retries: int = DEFAULT_MAX_RETRIES, timeout: float | Timeout | None | NotGiven = NOT_GIVEN, - transport: Transport | None = None, - proxies: ProxiesTypes | None = None, - limits: Limits | None = None, 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: - kwargs: dict[str, Any] = {} - if limits is not None: - warnings.warn( - "The `connection_pool_limits` argument is deprecated. The `http_client` argument should be passed instead", - category=DeprecationWarning, - stacklevel=3, - ) - if http_client is not None: - raise ValueError("The `http_client` argument is mutually exclusive with `connection_pool_limits`") - else: - limits = DEFAULT_CONNECTION_LIMITS - - if transport is not None: - kwargs["transport"] = transport - warnings.warn( - "The `transport` argument is deprecated. The `http_client` argument should be passed instead", - category=DeprecationWarning, - stacklevel=3, - ) - if http_client is not None: - raise ValueError("The `http_client` argument is mutually exclusive with `transport`") - - if proxies is not None: - kwargs["proxies"] = proxies - warnings.warn( - "The `proxies` argument is deprecated. The `http_client` argument should be passed instead", - category=DeprecationWarning, - stacklevel=3, - ) - if http_client is not None: - raise ValueError("The `http_client` argument is mutually exclusive with `proxies`") - 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. @@ -860,12 +812,9 @@ def __init__( super().__init__( version=version, - limits=limits, # cast to a valid type because mypy doesn't understand our type narrowing timeout=cast(Timeout, timeout), - proxies=proxies, base_url=base_url, - transport=transport, max_retries=max_retries, custom_query=custom_query, custom_headers=custom_headers, @@ -875,9 +824,6 @@ def __init__( base_url=base_url, # cast to a valid type because mypy doesn't understand our type narrowing timeout=cast(Timeout, timeout), - limits=limits, - follow_redirects=True, - **kwargs, # type: ignore ) def is_closed(self) -> bool: @@ -1372,45 +1318,10 @@ def __init__( _strict_response_validation: bool, max_retries: int = DEFAULT_MAX_RETRIES, timeout: float | Timeout | None | NotGiven = NOT_GIVEN, - transport: AsyncTransport | None = None, - proxies: ProxiesTypes | None = None, - limits: Limits | None = None, http_client: httpx.AsyncClient | None = None, custom_headers: Mapping[str, str] | None = None, custom_query: Mapping[str, object] | None = None, ) -> None: - kwargs: dict[str, Any] = {} - if limits is not None: - warnings.warn( - "The `connection_pool_limits` argument is deprecated. The `http_client` argument should be passed instead", - category=DeprecationWarning, - stacklevel=3, - ) - if http_client is not None: - raise ValueError("The `http_client` argument is mutually exclusive with `connection_pool_limits`") - else: - limits = DEFAULT_CONNECTION_LIMITS - - if transport is not None: - kwargs["transport"] = transport - warnings.warn( - "The `transport` argument is deprecated. The `http_client` argument should be passed instead", - category=DeprecationWarning, - stacklevel=3, - ) - if http_client is not None: - raise ValueError("The `http_client` argument is mutually exclusive with `transport`") - - if proxies is not None: - kwargs["proxies"] = proxies - warnings.warn( - "The `proxies` argument is deprecated. The `http_client` argument should be passed instead", - category=DeprecationWarning, - stacklevel=3, - ) - if http_client is not None: - raise ValueError("The `http_client` argument is mutually exclusive with `proxies`") - 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. @@ -1432,11 +1343,8 @@ def __init__( super().__init__( version=version, base_url=base_url, - limits=limits, # cast to a valid type because mypy doesn't understand our type narrowing timeout=cast(Timeout, timeout), - proxies=proxies, - transport=transport, max_retries=max_retries, custom_query=custom_query, custom_headers=custom_headers, @@ -1446,9 +1354,6 @@ def __init__( base_url=base_url, # cast to a valid type because mypy doesn't understand our type narrowing timeout=cast(Timeout, timeout), - limits=limits, - follow_redirects=True, - **kwargs, # type: ignore ) def is_closed(self) -> bool: From 523389c261a9912700622b91337eec4086897629 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 10 Mar 2025 22:23:21 +0000 Subject: [PATCH 095/330] feat(api): api update (#131) --- .stats.yml | 2 +- api.md | 10 ++- .../resources/sessions/sessions.py | 9 +-- src/browserbase/types/__init__.py | 1 + .../types/session_retrieve_response.py | 64 +++++++++++++++++++ tests/api_resources/test_sessions.py | 13 ++-- 6 files changed, 86 insertions(+), 13 deletions(-) create mode 100644 src/browserbase/types/session_retrieve_response.py diff --git a/.stats.yml b/.stats.yml index be077766..86eb81b9 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,2 +1,2 @@ configured_endpoints: 18 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fbrowserbase-396a2b9092f645c5a9e46a1f3be8c2e45ca9ae079e1d39761eb0a73f56e24b15.yml +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fbrowserbase-a42637317cf43a3f4dacf3b88ac09b86e41d4dc44c51140aa92cef99b5d0c02a.yml diff --git a/api.md b/api.md index 3f21eb29..dbb776f6 100644 --- a/api.md +++ b/api.md @@ -45,13 +45,19 @@ Methods: Types: ```python -from browserbase.types import Session, SessionLiveURLs, SessionCreateResponse, SessionListResponse +from browserbase.types import ( + Session, + SessionLiveURLs, + SessionCreateResponse, + SessionRetrieveResponse, + SessionListResponse, +) ``` Methods: - client.sessions.create(\*\*params) -> SessionCreateResponse -- client.sessions.retrieve(id) -> Session +- client.sessions.retrieve(id) -> SessionRetrieveResponse - client.sessions.update(id, \*\*params) -> Session - client.sessions.list(\*\*params) -> SessionListResponse - client.sessions.debug(id) -> SessionLiveURLs diff --git a/src/browserbase/resources/sessions/sessions.py b/src/browserbase/resources/sessions/sessions.py index 0572d913..c1710e22 100644 --- a/src/browserbase/resources/sessions/sessions.py +++ b/src/browserbase/resources/sessions/sessions.py @@ -58,6 +58,7 @@ from ...types.session_live_urls import SessionLiveURLs from ...types.session_list_response import SessionListResponse from ...types.session_create_response import SessionCreateResponse +from ...types.session_retrieve_response import SessionRetrieveResponse __all__ = ["SessionsResource", "AsyncSessionsResource"] @@ -180,7 +181,7 @@ def retrieve( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> Session: + ) -> SessionRetrieveResponse: """ Session @@ -200,7 +201,7 @@ def retrieve( options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=Session, + cast_to=SessionRetrieveResponse, ) def update( @@ -451,7 +452,7 @@ async def retrieve( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> Session: + ) -> SessionRetrieveResponse: """ Session @@ -471,7 +472,7 @@ async def retrieve( options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=Session, + cast_to=SessionRetrieveResponse, ) async def update( diff --git a/src/browserbase/types/__init__.py b/src/browserbase/types/__init__.py index ebc243db..4dd85ddb 100644 --- a/src/browserbase/types/__init__.py +++ b/src/browserbase/types/__init__.py @@ -18,3 +18,4 @@ from .context_update_response import ContextUpdateResponse as ContextUpdateResponse from .extension_create_params import ExtensionCreateParams as ExtensionCreateParams from .session_create_response import SessionCreateResponse as SessionCreateResponse +from .session_retrieve_response import SessionRetrieveResponse as SessionRetrieveResponse diff --git a/src/browserbase/types/session_retrieve_response.py b/src/browserbase/types/session_retrieve_response.py new file mode 100644 index 00000000..a9a4ff28 --- /dev/null +++ b/src/browserbase/types/session_retrieve_response.py @@ -0,0 +1,64 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Dict, Optional +from datetime import datetime +from typing_extensions import Literal + +from pydantic import Field as FieldInfo + +from .._models import BaseModel + +__all__ = ["SessionRetrieveResponse"] + + +class SessionRetrieveResponse(BaseModel): + id: str + + created_at: datetime = FieldInfo(alias="createdAt") + + expires_at: datetime = FieldInfo(alias="expiresAt") + + keep_alive: bool = FieldInfo(alias="keepAlive") + """Indicates if the Session was created to be kept alive upon disconnections""" + + project_id: str = FieldInfo(alias="projectId") + """The Project ID linked to the Session.""" + + proxy_bytes: int = FieldInfo(alias="proxyBytes") + """Bytes used via the [Proxy](/features/stealth-mode#proxies-and-residential-ips)""" + + region: Literal["us-west-2", "us-east-1", "eu-central-1", "ap-southeast-1"] + """The region where the Session is running.""" + + started_at: datetime = FieldInfo(alias="startedAt") + + status: Literal["RUNNING", "ERROR", "TIMED_OUT", "COMPLETED"] + + updated_at: datetime = FieldInfo(alias="updatedAt") + + avg_cpu_usage: Optional[int] = FieldInfo(alias="avgCpuUsage", default=None) + """CPU used by the Session""" + + connect_url: Optional[str] = FieldInfo(alias="connectUrl", default=None) + """WebSocket URL to connect to the Session.""" + + context_id: Optional[str] = FieldInfo(alias="contextId", default=None) + """Optional. The Context linked to the Session.""" + + ended_at: Optional[datetime] = FieldInfo(alias="endedAt", default=None) + + memory_usage: Optional[int] = FieldInfo(alias="memoryUsage", default=None) + """Memory used by the Session""" + + selenium_remote_url: Optional[str] = FieldInfo(alias="seleniumRemoteUrl", default=None) + """HTTP URL to connect to the Session.""" + + signing_key: Optional[str] = FieldInfo(alias="signingKey", default=None) + """Signing key to use when connecting to the Session via HTTP.""" + + user_metadata: Optional[Dict[str, object]] = FieldInfo(alias="userMetadata", default=None) + """Arbitrary user metadata to attach to the session. + + To learn more about user metadata, see + [User Metadata](/features/sessions#user-metadata). + """ diff --git a/tests/api_resources/test_sessions.py b/tests/api_resources/test_sessions.py index 482624f8..853a5910 100644 --- a/tests/api_resources/test_sessions.py +++ b/tests/api_resources/test_sessions.py @@ -14,6 +14,7 @@ SessionLiveURLs, SessionListResponse, SessionCreateResponse, + SessionRetrieveResponse, ) base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") @@ -100,7 +101,7 @@ def test_method_retrieve(self, client: Browserbase) -> None: session = client.sessions.retrieve( "id", ) - assert_matches_type(Session, session, path=["response"]) + assert_matches_type(SessionRetrieveResponse, session, path=["response"]) @parametrize def test_raw_response_retrieve(self, client: Browserbase) -> None: @@ -111,7 +112,7 @@ def test_raw_response_retrieve(self, client: Browserbase) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" session = response.parse() - assert_matches_type(Session, session, path=["response"]) + assert_matches_type(SessionRetrieveResponse, session, path=["response"]) @parametrize def test_streaming_response_retrieve(self, client: Browserbase) -> None: @@ -122,7 +123,7 @@ def test_streaming_response_retrieve(self, client: Browserbase) -> None: assert response.http_request.headers.get("X-Stainless-Lang") == "python" session = response.parse() - assert_matches_type(Session, session, path=["response"]) + assert_matches_type(SessionRetrieveResponse, session, path=["response"]) assert cast(Any, response.is_closed) is True @@ -332,7 +333,7 @@ async def test_method_retrieve(self, async_client: AsyncBrowserbase) -> None: session = await async_client.sessions.retrieve( "id", ) - assert_matches_type(Session, session, path=["response"]) + assert_matches_type(SessionRetrieveResponse, session, path=["response"]) @parametrize async def test_raw_response_retrieve(self, async_client: AsyncBrowserbase) -> None: @@ -343,7 +344,7 @@ async def test_raw_response_retrieve(self, async_client: AsyncBrowserbase) -> No assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" session = await response.parse() - assert_matches_type(Session, session, path=["response"]) + assert_matches_type(SessionRetrieveResponse, session, path=["response"]) @parametrize async def test_streaming_response_retrieve(self, async_client: AsyncBrowserbase) -> None: @@ -354,7 +355,7 @@ async def test_streaming_response_retrieve(self, async_client: AsyncBrowserbase) assert response.http_request.headers.get("X-Stainless-Lang") == "python" session = await response.parse() - assert_matches_type(Session, session, path=["response"]) + assert_matches_type(SessionRetrieveResponse, session, path=["response"]) assert cast(Any, response.is_closed) is True From 17f263b9df973aae5bd308753a2907f2ea43d7d1 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 12 Mar 2025 02:41:58 +0000 Subject: [PATCH 096/330] chore(internal): codegen related update (#132) --- README.md | 44 ++++++++++++++++++++++++++++++++++++++++++++ scripts/test | 2 ++ 2 files changed, 46 insertions(+) diff --git a/README.md b/README.md index 75d6311c..072b8668 100644 --- a/README.md +++ b/README.md @@ -77,6 +77,50 @@ 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 browserbase import Browserbase + +client = Browserbase() + +session = client.sessions.create( + project_id="projectId", + 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, + }, + }, +) +print(session.browser_settings) +``` + ## File uploads Request parameters that correspond to file uploads can be passed as `bytes`, a [`PathLike`](https://docs.python.org/3/library/os.html#os.PathLike) instance or a tuple of `(filename, contents, media type)`. diff --git a/scripts/test b/scripts/test index 4fa5698b..2b878456 100755 --- a/scripts/test +++ b/scripts/test @@ -52,6 +52,8 @@ else echo fi +export DEFER_PYDANTIC_BUILD=false + echo "==> Running tests" rye run pytest "$@" From 0ffb3b6b93da624c87d1677cb8a5172d79ba30db Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 12 Mar 2025 08:12:34 +0000 Subject: [PATCH 097/330] feat(api): api update (#133) --- .stats.yml | 2 +- README.md | 2 +- src/browserbase/types/session_create_params.py | 2 +- tests/api_resources/test_sessions.py | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.stats.yml b/.stats.yml index 86eb81b9..662fd770 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,2 +1,2 @@ configured_endpoints: 18 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fbrowserbase-a42637317cf43a3f4dacf3b88ac09b86e41d4dc44c51140aa92cef99b5d0c02a.yml +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fbrowserbase-06206dff3ffaf539b0ed8aa5bac368cb366b631f85d7a4ba5f07aca91c172550.yml diff --git a/README.md b/README.md index 072b8668..4cb5190f 100644 --- a/README.md +++ b/README.md @@ -99,7 +99,7 @@ session = client.sessions.create( "fingerprint": { "browsers": ["chrome"], "devices": ["desktop"], - "http_version": 1, + "http_version": "1", "locales": ["string"], "operating_systems": ["android"], "screen": { diff --git a/src/browserbase/types/session_create_params.py b/src/browserbase/types/session_create_params.py index 5e76037d..ab290943 100644 --- a/src/browserbase/types/session_create_params.py +++ b/src/browserbase/types/session_create_params.py @@ -88,7 +88,7 @@ class BrowserSettingsFingerprint(TypedDict, total=False): devices: List[Literal["desktop", "mobile"]] - http_version: Annotated[Literal[1, 2], PropertyInfo(alias="httpVersion")] + http_version: Annotated[Literal["1", "2"], PropertyInfo(alias="httpVersion")] locales: List[str] """ diff --git a/tests/api_resources/test_sessions.py b/tests/api_resources/test_sessions.py index 853a5910..0581655c 100644 --- a/tests/api_resources/test_sessions.py +++ b/tests/api_resources/test_sessions.py @@ -45,7 +45,7 @@ def test_method_create_with_all_params(self, client: Browserbase) -> None: "fingerprint": { "browsers": ["chrome"], "devices": ["desktop"], - "http_version": 1, + "http_version": "1", "locales": ["string"], "operating_systems": ["android"], "screen": { @@ -277,7 +277,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncBrowserbas "fingerprint": { "browsers": ["chrome"], "devices": ["desktop"], - "http_version": 1, + "http_version": "1", "locales": ["string"], "operating_systems": ["android"], "screen": { From 68cba0621229cab560129df42c6a2f7948d939c9 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 14 Mar 2025 04:44:12 +0000 Subject: [PATCH 098/330] chore(internal): remove extra empty newlines (#134) --- pyproject.toml | 2 -- 1 file changed, 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 69e53d56..b6fca0be 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,7 +38,6 @@ Homepage = "https://github.com/browserbase/sdk-python" Repository = "https://github.com/browserbase/sdk-python" - [tool.rye] managed = true # version pins are in requirements-dev.lock @@ -152,7 +151,6 @@ reportImplicitOverride = true reportImportCycles = false reportPrivateUsage = false - [tool.ruff] line-length = 120 output-format = "grouped" From 0908b26f0c46ce54306dc299777e01031b1cb8a4 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 15 Mar 2025 04:10:30 +0000 Subject: [PATCH 099/330] chore(internal): codegen related update (#135) --- requirements-dev.lock | 1 + requirements.lock | 1 + 2 files changed, 2 insertions(+) diff --git a/requirements-dev.lock b/requirements-dev.lock index 2ad2c995..3a951fec 100644 --- a/requirements-dev.lock +++ b/requirements-dev.lock @@ -7,6 +7,7 @@ # all-features: true # with-sources: false # generate-hashes: false +# universal: false -e file:. annotated-types==0.6.0 diff --git a/requirements.lock b/requirements.lock index fea30548..9efa54d7 100644 --- a/requirements.lock +++ b/requirements.lock @@ -7,6 +7,7 @@ # all-features: true # with-sources: false # generate-hashes: false +# universal: false -e file:. annotated-types==0.6.0 From 6e8c23cdb1daf2a57b7df6910c36caaf63013299 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 15 Mar 2025 04:15:04 +0000 Subject: [PATCH 100/330] chore(internal): bump rye to 0.44.0 (#136) --- .devcontainer/Dockerfile | 2 +- .github/workflows/ci.yml | 4 ++-- .github/workflows/publish-pypi.yml | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 55d20255..ff261bad 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -3,7 +3,7 @@ FROM mcr.microsoft.com/vscode/devcontainers/python:0-${VARIANT} USER vscode -RUN curl -sSf https://rye.astral.sh/get | RYE_VERSION="0.35.0" RYE_INSTALL_OPTION="--yes" bash +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/.github/workflows/ci.yml b/.github/workflows/ci.yml index c8a8a4f7..3b286e5a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,7 +21,7 @@ jobs: curl -sSf https://rye.astral.sh/get | bash echo "$HOME/.rye/shims" >> $GITHUB_PATH env: - RYE_VERSION: '0.35.0' + RYE_VERSION: '0.44.0' RYE_INSTALL_OPTION: '--yes' - name: Install dependencies @@ -42,7 +42,7 @@ jobs: curl -sSf https://rye.astral.sh/get | bash echo "$HOME/.rye/shims" >> $GITHUB_PATH env: - RYE_VERSION: '0.35.0' + RYE_VERSION: '0.44.0' RYE_INSTALL_OPTION: '--yes' - name: Bootstrap diff --git a/.github/workflows/publish-pypi.yml b/.github/workflows/publish-pypi.yml index db8cf944..b3c832c7 100644 --- a/.github/workflows/publish-pypi.yml +++ b/.github/workflows/publish-pypi.yml @@ -21,7 +21,7 @@ jobs: curl -sSf https://rye.astral.sh/get | bash echo "$HOME/.rye/shims" >> $GITHUB_PATH env: - RYE_VERSION: '0.35.0' + RYE_VERSION: '0.44.0' RYE_INSTALL_OPTION: '--yes' - name: Publish to PyPI From 1e91f75cade8b753672b0c90390a3599068acb5b Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 15 Mar 2025 04:21:08 +0000 Subject: [PATCH 101/330] fix(types): handle more discriminated union shapes (#137) --- src/browserbase/_models.py | 7 +++++-- tests/test_models.py | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 2 deletions(-) diff --git a/src/browserbase/_models.py b/src/browserbase/_models.py index c4401ff8..b51a1bf5 100644 --- a/src/browserbase/_models.py +++ b/src/browserbase/_models.py @@ -65,7 +65,7 @@ from ._constants import RAW_RESPONSE_HEADER if TYPE_CHECKING: - from pydantic_core.core_schema import ModelField, LiteralSchema, ModelFieldsSchema + from pydantic_core.core_schema import ModelField, ModelSchema, LiteralSchema, ModelFieldsSchema __all__ = ["BaseModel", "GenericModel"] @@ -646,15 +646,18 @@ def _build_discriminated_union_meta(*, union: type, meta_annotations: tuple[Any, 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 diff --git a/tests/test_models.py b/tests/test_models.py index 669d2190..21043abd 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -854,3 +854,35 @@ class Model(BaseModel): 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) From c6dcd9928b8749d46a07f49cd59c282aed6888f2 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 17 Mar 2025 16:24:04 +0000 Subject: [PATCH 102/330] fix(ci): ensure pip is always available (#138) --- bin/publish-pypi | 1 + 1 file changed, 1 insertion(+) diff --git a/bin/publish-pypi b/bin/publish-pypi index 05bfccbb..ebebf916 100644 --- a/bin/publish-pypi +++ b/bin/publish-pypi @@ -5,5 +5,6 @@ mkdir -p dist rye build --clean # Patching importlib-metadata version until upstream library version is updated # https://github.com/pypa/twine/issues/977#issuecomment-2189800841 +"$HOME/.rye/self/bin/python3" -m ensurepip "$HOME/.rye/self/bin/python3" -m pip install 'importlib-metadata==7.2.1' rye publish --yes --token=$PYPI_TOKEN From 36ef90cb6c9afa864d2375806a74b28abefe2b50 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 17 Mar 2025 16:30:42 +0000 Subject: [PATCH 103/330] fix(ci): remove publishing patch (#139) --- bin/publish-pypi | 4 ---- pyproject.toml | 2 +- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/bin/publish-pypi b/bin/publish-pypi index ebebf916..826054e9 100644 --- a/bin/publish-pypi +++ b/bin/publish-pypi @@ -3,8 +3,4 @@ set -eux mkdir -p dist rye build --clean -# Patching importlib-metadata version until upstream library version is updated -# https://github.com/pypa/twine/issues/977#issuecomment-2189800841 -"$HOME/.rye/self/bin/python3" -m ensurepip -"$HOME/.rye/self/bin/python3" -m pip install 'importlib-metadata==7.2.1' rye publish --yes --token=$PYPI_TOKEN diff --git a/pyproject.toml b/pyproject.toml index b6fca0be..91476dfb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -86,7 +86,7 @@ typecheck = { chain = [ "typecheck:mypy" = "mypy ." [build-system] -requires = ["hatchling", "hatch-fancy-pypi-readme"] +requires = ["hatchling==1.26.3", "hatch-fancy-pypi-readme"] build-backend = "hatchling.build" [tool.hatch.build] From e53ef4594db83699c91037a32c777bbf1df86723 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 20 Mar 2025 16:29:43 +0000 Subject: [PATCH 104/330] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index 662fd770..c2b2a7ad 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,2 +1,2 @@ configured_endpoints: 18 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fbrowserbase-06206dff3ffaf539b0ed8aa5bac368cb366b631f85d7a4ba5f07aca91c172550.yml +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fbrowserbase-ec3d6c02f952f61a904c5fe7890827b8bf9add878e772a96dbdafb328057ad31.yml From 8b85299ed078797a75b0805342959d88e605533a Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 20 Mar 2025 23:08:50 +0000 Subject: [PATCH 105/330] feat(api): api update (#140) --- .stats.yml | 2 +- src/browserbase/types/project.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index c2b2a7ad..532fc03a 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,2 +1,2 @@ configured_endpoints: 18 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fbrowserbase-ec3d6c02f952f61a904c5fe7890827b8bf9add878e772a96dbdafb328057ad31.yml +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fbrowserbase-d3cf17b167ebc2abbcbe668904c4e2b69b1697b340249afbcffcd329366eea34.yml diff --git a/src/browserbase/types/project.py b/src/browserbase/types/project.py index afbcef63..112e719d 100644 --- a/src/browserbase/types/project.py +++ b/src/browserbase/types/project.py @@ -12,6 +12,8 @@ class Project(BaseModel): id: str + concurrency: int + created_at: datetime = FieldInfo(alias="createdAt") default_timeout: int = FieldInfo(alias="defaultTimeout") From dc8f944fcdfc3d4e288f30a296aee7dc78d86fa2 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 21 Mar 2025 20:34:05 +0000 Subject: [PATCH 106/330] feat(api): api update (#141) --- .stats.yml | 2 +- src/browserbase/types/project.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index 532fc03a..89c32026 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,2 +1,2 @@ configured_endpoints: 18 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fbrowserbase-d3cf17b167ebc2abbcbe668904c4e2b69b1697b340249afbcffcd329366eea34.yml +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fbrowserbase-ad8e080c2347b3f28d64f49cf02c2ab4a69b7bf289fd7eb018c955d8915bb990.yml diff --git a/src/browserbase/types/project.py b/src/browserbase/types/project.py index 112e719d..dc3cf335 100644 --- a/src/browserbase/types/project.py +++ b/src/browserbase/types/project.py @@ -13,6 +13,7 @@ class Project(BaseModel): id: str concurrency: int + """The maximum number of sessions that this project can run concurrently.""" created_at: datetime = FieldInfo(alias="createdAt") From 74c5df992cde82faf7e52d76dfc0a77c23890000 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 27 Mar 2025 03:33:02 +0000 Subject: [PATCH 107/330] chore: fix typos (#142) --- src/browserbase/_models.py | 2 +- src/browserbase/_utils/_transform.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/browserbase/_models.py b/src/browserbase/_models.py index b51a1bf5..34935716 100644 --- a/src/browserbase/_models.py +++ b/src/browserbase/_models.py @@ -681,7 +681,7 @@ def set_pydantic_config(typ: Any, config: pydantic.ConfigDict) -> None: setattr(typ, "__pydantic_config__", config) # noqa: B010 -# our use of subclasssing here causes weirdness for type checkers, +# our use of subclassing here causes weirdness for type checkers, # so we just pretend that we don't subclass if TYPE_CHECKING: GenericModel = BaseModel diff --git a/src/browserbase/_utils/_transform.py b/src/browserbase/_utils/_transform.py index 18afd9d8..7ac2e17f 100644 --- a/src/browserbase/_utils/_transform.py +++ b/src/browserbase/_utils/_transform.py @@ -126,7 +126,7 @@ def _get_annotated_type(type_: type) -> type | 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 `PropertInfo` metadata. + Note: this function only looks at `Annotated` types that contain `PropertyInfo` metadata. """ annotated_type = _get_annotated_type(type_) if annotated_type is None: From 0ae8c61597e23f89e834e61e474ce08dc91387fd Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 27 Mar 2025 03:34:00 +0000 Subject: [PATCH 108/330] codegen metadata --- .stats.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.stats.yml b/.stats.yml index 89c32026..072ee700 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,2 +1,4 @@ configured_endpoints: 18 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fbrowserbase-ad8e080c2347b3f28d64f49cf02c2ab4a69b7bf289fd7eb018c955d8915bb990.yml +openapi_spec_hash: b3aea10135a89597634d62f1ef418839 +config_hash: 74882e23a455dece33e43a27e67f0fbb From 65942d6a8b2c9d9156a345427e66b762be00de0d Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 1 Apr 2025 19:52:19 +0000 Subject: [PATCH 109/330] feat(api): api update (#143) --- .stats.yml | 4 ++-- src/browserbase/resources/sessions/sessions.py | 8 ++++---- src/browserbase/types/session_create_params.py | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.stats.yml b/.stats.yml index 072ee700..92128df9 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 18 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fbrowserbase-ad8e080c2347b3f28d64f49cf02c2ab4a69b7bf289fd7eb018c955d8915bb990.yml -openapi_spec_hash: b3aea10135a89597634d62f1ef418839 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fbrowserbase-45328621800082e652c9b2f34b176b89ebba3af423ea9f4fed91a359cf4e0ae4.yml +openapi_spec_hash: c20658b49312b14d158ce5c88f34ee34 config_hash: 74882e23a455dece33e43a27e67f0fbb diff --git a/src/browserbase/resources/sessions/sessions.py b/src/browserbase/resources/sessions/sessions.py index c1710e22..44814380 100644 --- a/src/browserbase/resources/sessions/sessions.py +++ b/src/browserbase/resources/sessions/sessions.py @@ -128,8 +128,8 @@ def create( extension_id: The uploaded Extension ID. See [Upload Extension](/reference/api/upload-an-extension). - keep_alive: Set to true to keep the session alive even after disconnections. This is - available on the Startup plan only. + keep_alive: Set to true to keep the session alive even after disconnections. Available on + the Hobby Plan and above. proxies: Proxy configuration. Can be true for default proxy, or an array of proxy configurations. @@ -399,8 +399,8 @@ async def create( extension_id: The uploaded Extension ID. See [Upload Extension](/reference/api/upload-an-extension). - keep_alive: Set to true to keep the session alive even after disconnections. This is - available on the Startup plan only. + keep_alive: Set to true to keep the session alive even after disconnections. Available on + the Hobby Plan and above. proxies: Proxy configuration. Can be true for default proxy, or an array of proxy configurations. diff --git a/src/browserbase/types/session_create_params.py b/src/browserbase/types/session_create_params.py index ab290943..f3b9606d 100644 --- a/src/browserbase/types/session_create_params.py +++ b/src/browserbase/types/session_create_params.py @@ -39,7 +39,7 @@ class SessionCreateParams(TypedDict, total=False): keep_alive: Annotated[bool, PropertyInfo(alias="keepAlive")] """Set to true to keep the session alive even after disconnections. - This is available on the Startup plan only. + Available on the Hobby Plan and above. """ proxies: Union[bool, Iterable[ProxiesUnionMember1]] From 5624ac937ca7d77c3ca2d74f3f9bf9d5e4ed3919 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 4 Apr 2025 08:06:33 +0000 Subject: [PATCH 110/330] chore(internal): remove trailing character (#145) --- tests/test_client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_client.py b/tests/test_client.py index 1c703f0b..d03654df 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -1627,7 +1627,7 @@ def test_get_platform(self) -> None: import threading from browserbase._utils import asyncify - from browserbase._base_client import get_platform + from browserbase._base_client import get_platform async def test_main() -> None: result = await asyncify(get_platform)() From 336bbb6943f5494a6c900687a5b00a4d033230b0 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 9 Apr 2025 02:29:09 +0000 Subject: [PATCH 111/330] chore(internal): slight transform perf improvement (#147) --- src/browserbase/_utils/_transform.py | 22 ++++++++++++++++++++++ tests/test_transform.py | 12 ++++++++++++ 2 files changed, 34 insertions(+) diff --git a/src/browserbase/_utils/_transform.py b/src/browserbase/_utils/_transform.py index 7ac2e17f..3ec62081 100644 --- a/src/browserbase/_utils/_transform.py +++ b/src/browserbase/_utils/_transform.py @@ -142,6 +142,10 @@ def _maybe_transform_key(key: str, type_: type) -> str: return key +def _no_transform_needed(annotation: type) -> bool: + return annotation == float or annotation == int + + def _transform_recursive( data: object, *, @@ -184,6 +188,15 @@ def _transform_recursive( 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): @@ -332,6 +345,15 @@ async def _async_transform_recursive( 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): diff --git a/tests/test_transform.py b/tests/test_transform.py index 32c44ae7..85cabc35 100644 --- a/tests/test_transform.py +++ b/tests/test_transform.py @@ -432,3 +432,15 @@ async def test_base64_file_input(use_async: bool) -> None: 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] From 8718057fed5e50f5bc50d75e5d0d756d87a96a05 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 9 Apr 2025 02:33:57 +0000 Subject: [PATCH 112/330] chore: slight wording improvement in README (#148) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4cb5190f..c99b365c 100644 --- a/README.md +++ b/README.md @@ -123,7 +123,7 @@ print(session.browser_settings) ## File uploads -Request parameters that correspond to file uploads can be passed as `bytes`, a [`PathLike`](https://docs.python.org/3/library/os.html#os.PathLike) instance or a tuple of `(filename, contents, media type)`. +Request parameters that correspond to file uploads can be passed as `bytes`, or a [`PathLike`](https://docs.python.org/3/library/os.html#os.PathLike) instance or a tuple of `(filename, contents, media type)`. ```python from pathlib import Path From c1e5579449f405de34e7f9ec60d4b8858285ced0 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 10 Apr 2025 02:34:55 +0000 Subject: [PATCH 113/330] chore(internal): expand CI branch coverage --- .github/workflows/ci.yml | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3b286e5a..53a3a09c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,18 +1,18 @@ name: CI on: push: - branches: - - main - pull_request: - branches: - - main - - next + branches-ignore: + - 'generated' + - 'codegen/**' + - 'integrated/**' + - 'preview-head/**' + - 'preview-base/**' + - 'preview/**' jobs: lint: name: lint runs-on: ubuntu-latest - steps: - uses: actions/checkout@v4 @@ -33,7 +33,6 @@ jobs: test: name: test runs-on: ubuntu-latest - steps: - uses: actions/checkout@v4 From f41f2bc0e79b62f95687ba08c328fe61f26175ed Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 10 Apr 2025 02:38:14 +0000 Subject: [PATCH 114/330] chore(internal): reduce CI branch coverage --- .github/workflows/ci.yml | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 53a3a09c..81f6dc20 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,13 +1,12 @@ name: CI on: push: - branches-ignore: - - 'generated' - - 'codegen/**' - - 'integrated/**' - - 'preview-head/**' - - 'preview-base/**' - - 'preview/**' + branches: + - main + pull_request: + branches: + - main + - next jobs: lint: From 375631cac4101d017b9e61ea63a3d30f319ae96f Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 12 Apr 2025 02:34:00 +0000 Subject: [PATCH 115/330] fix(perf): skip traversing types for NotGiven values --- src/browserbase/_utils/_transform.py | 11 +++++++++++ tests/test_transform.py | 9 ++++++++- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/browserbase/_utils/_transform.py b/src/browserbase/_utils/_transform.py index 3ec62081..3b2b8e00 100644 --- a/src/browserbase/_utils/_transform.py +++ b/src/browserbase/_utils/_transform.py @@ -12,6 +12,7 @@ from ._utils import ( is_list, + is_given, is_mapping, is_iterable, ) @@ -258,6 +259,11 @@ def _transform_typeddict( 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 `NotGiven` 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 @@ -415,6 +421,11 @@ async def _async_transform_typeddict( 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 `NotGiven` 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 diff --git a/tests/test_transform.py b/tests/test_transform.py index 85cabc35..cba80b21 100644 --- a/tests/test_transform.py +++ b/tests/test_transform.py @@ -8,7 +8,7 @@ import pytest -from browserbase._types import Base64FileInput +from browserbase._types import NOT_GIVEN, Base64FileInput from browserbase._utils import ( PropertyInfo, transform as _transform, @@ -444,3 +444,10 @@ async def test_transform_skipping(use_async: bool) -> None: # 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) == {} From 2f40460609c1f64657972b1e3457f8a08bf7c5fb Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 12 Apr 2025 02:35:03 +0000 Subject: [PATCH 116/330] fix(perf): optimize some hot paths --- src/browserbase/_utils/_transform.py | 14 +++++++++++++- src/browserbase/_utils/_typing.py | 2 ++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/browserbase/_utils/_transform.py b/src/browserbase/_utils/_transform.py index 3b2b8e00..b0cc20a7 100644 --- a/src/browserbase/_utils/_transform.py +++ b/src/browserbase/_utils/_transform.py @@ -5,7 +5,7 @@ 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 +from typing_extensions import Literal, get_args, override, get_type_hints as _get_type_hints import anyio import pydantic @@ -13,6 +13,7 @@ from ._utils import ( is_list, is_given, + lru_cache, is_mapping, is_iterable, ) @@ -109,6 +110,7 @@ class Params(TypedDict, total=False): 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. @@ -433,3 +435,13 @@ async def _async_transform_typeddict( 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/browserbase/_utils/_typing.py b/src/browserbase/_utils/_typing.py index 278749b1..1958820f 100644 --- a/src/browserbase/_utils/_typing.py +++ b/src/browserbase/_utils/_typing.py @@ -13,6 +13,7 @@ get_origin, ) +from ._utils import lru_cache from .._types import InheritsGeneric from .._compat import is_union as _is_union @@ -66,6 +67,7 @@ def is_type_alias_type(tp: Any, /) -> TypeIs[typing_extensions.TypeAliasType]: # 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])) From 09b122771739ea465d03d25b48509b69f0547387 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 15 Apr 2025 02:45:54 +0000 Subject: [PATCH 117/330] chore(internal): update pyright settings --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 91476dfb..3b2de47c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -147,6 +147,7 @@ exclude = [ ] reportImplicitOverride = true +reportOverlappingOverload = false reportImportCycles = false reportPrivateUsage = false From 54ece628bf632c0ba10b6cb5d7407797480153e8 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 15 Apr 2025 02:47:12 +0000 Subject: [PATCH 118/330] chore(client): minor internal fixes --- src/browserbase/_base_client.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/browserbase/_base_client.py b/src/browserbase/_base_client.py index 2956496d..e30de649 100644 --- a/src/browserbase/_base_client.py +++ b/src/browserbase/_base_client.py @@ -409,7 +409,8 @@ def _build_headers(self, options: FinalRequestOptions, *, retries_taken: int = 0 idempotency_header = self._idempotency_header if idempotency_header and options.method.lower() != "get" and idempotency_header not in headers: - headers[idempotency_header] = options.idempotency_key or self._idempotency_key() + options.idempotency_key = options.idempotency_key or self._idempotency_key() + 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. @@ -943,6 +944,10 @@ def _request( request = self._build_request(options, retries_taken=retries_taken) self._prepare_request(request) + if options.idempotency_key: + # ensure the idempotency key is reused between requests + input_options.idempotency_key = options.idempotency_key + kwargs: HttpxSendArgs = {} if self.custom_auth is not None: kwargs["auth"] = self.custom_auth @@ -1475,6 +1480,10 @@ async def _request( request = self._build_request(options, retries_taken=retries_taken) await self._prepare_request(request) + if options.idempotency_key: + # ensure the idempotency key is reused between requests + input_options.idempotency_key = options.idempotency_key + kwargs: HttpxSendArgs = {} if self.custom_auth is not None: kwargs["auth"] = self.custom_auth From 6c245ad1a4380743802777d54c59e026822dbbdb Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 17 Apr 2025 02:40:27 +0000 Subject: [PATCH 119/330] chore(internal): bump pyright version --- pyproject.toml | 2 +- requirements-dev.lock | 2 +- src/browserbase/_base_client.py | 6 +++++- src/browserbase/_models.py | 1 - src/browserbase/_utils/_typing.py | 2 +- tests/conftest.py | 2 +- tests/test_models.py | 2 +- 7 files changed, 10 insertions(+), 7 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 3b2de47c..e70dab6b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,7 +42,7 @@ Repository = "https://github.com/browserbase/sdk-python" managed = true # version pins are in requirements-dev.lock dev-dependencies = [ - "pyright>=1.1.359", + "pyright==1.1.399", "mypy", "respx", "pytest", diff --git a/requirements-dev.lock b/requirements-dev.lock index 3a951fec..a26bd10f 100644 --- a/requirements-dev.lock +++ b/requirements-dev.lock @@ -69,7 +69,7 @@ pydantic-core==2.27.1 # via pydantic pygments==2.18.0 # via rich -pyright==1.1.392.post0 +pyright==1.1.399 pytest==8.3.3 # via pytest-asyncio pytest-asyncio==0.24.0 diff --git a/src/browserbase/_base_client.py b/src/browserbase/_base_client.py index e30de649..f25497c7 100644 --- a/src/browserbase/_base_client.py +++ b/src/browserbase/_base_client.py @@ -98,7 +98,11 @@ _AsyncStreamT = TypeVar("_AsyncStreamT", bound=AsyncStream[Any]) if TYPE_CHECKING: - from httpx._config import DEFAULT_TIMEOUT_CONFIG as HTTPX_DEFAULT_TIMEOUT + 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 diff --git a/src/browserbase/_models.py b/src/browserbase/_models.py index 34935716..58b9263e 100644 --- a/src/browserbase/_models.py +++ b/src/browserbase/_models.py @@ -19,7 +19,6 @@ ) import pydantic -import pydantic.generics from pydantic.fields import FieldInfo from ._types import ( diff --git a/src/browserbase/_utils/_typing.py b/src/browserbase/_utils/_typing.py index 1958820f..1bac9542 100644 --- a/src/browserbase/_utils/_typing.py +++ b/src/browserbase/_utils/_typing.py @@ -110,7 +110,7 @@ class MyResponse(Foo[_T]): ``` """ cls = cast(object, get_origin(typ) or typ) - if cls in generic_bases: + if cls in generic_bases: # pyright: ignore[reportUnnecessaryContains] # we're given the class directly return extract_type_arg(typ, index) diff --git a/tests/conftest.py b/tests/conftest.py index 15ddbcad..94b8e723 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -10,7 +10,7 @@ from browserbase import Browserbase, AsyncBrowserbase if TYPE_CHECKING: - from _pytest.fixtures import FixtureRequest + from _pytest.fixtures import FixtureRequest # pyright: ignore[reportPrivateImportUsage] pytest.register_assert_rewrite("tests.utils") diff --git a/tests/test_models.py b/tests/test_models.py index 21043abd..f2a9c346 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -832,7 +832,7 @@ class B(BaseModel): @pytest.mark.skipif(not PYDANTIC_V2, reason="TypeAliasType is not supported in Pydantic v1") def test_type_alias_type() -> None: - Alias = TypeAliasType("Alias", str) + Alias = TypeAliasType("Alias", str) # pyright: ignore class Model(BaseModel): alias: Alias From 7e71ed828b7211ba4fde6ed8d5bda1f09e3b8321 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 17 Apr 2025 02:41:01 +0000 Subject: [PATCH 120/330] chore(internal): base client updates --- src/browserbase/_base_client.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/src/browserbase/_base_client.py b/src/browserbase/_base_client.py index f25497c7..20317b17 100644 --- a/src/browserbase/_base_client.py +++ b/src/browserbase/_base_client.py @@ -119,6 +119,7 @@ class PageInfo: url: URL | NotGiven params: Query | NotGiven + json: Body | NotGiven @overload def __init__( @@ -134,19 +135,30 @@ def __init__( 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})" @@ -195,6 +207,19 @@ def _info_to_options(self, info: PageInfo) -> FinalRequestOptions: 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") From f4648e7c4102ac04fb25bb0ed47624607adebda3 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 19 Apr 2025 02:43:50 +0000 Subject: [PATCH 121/330] chore(internal): update models test --- tests/test_models.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/test_models.py b/tests/test_models.py index f2a9c346..b5335f94 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -492,12 +492,15 @@ 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 From d9f763d89e0b98e424211437929acfa0210c6bc2 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 23 Apr 2025 03:15:16 +0000 Subject: [PATCH 122/330] chore(ci): add timeout thresholds for CI jobs --- .github/workflows/ci.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 81f6dc20..04b083ca 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,6 +10,7 @@ on: jobs: lint: + timeout-minutes: 10 name: lint runs-on: ubuntu-latest steps: @@ -30,6 +31,7 @@ jobs: run: ./scripts/lint test: + timeout-minutes: 10 name: test runs-on: ubuntu-latest steps: From 5757c05360fb0de3c8c03b9d6e07f0720b59ef67 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 23 Apr 2025 03:15:40 +0000 Subject: [PATCH 123/330] chore(internal): import reformatting --- src/browserbase/_client.py | 5 +---- src/browserbase/resources/contexts.py | 5 +---- src/browserbase/resources/extensions.py | 7 +------ src/browserbase/resources/sessions/sessions.py | 5 +---- src/browserbase/resources/sessions/uploads.py | 7 +------ 5 files changed, 5 insertions(+), 24 deletions(-) diff --git a/src/browserbase/_client.py b/src/browserbase/_client.py index 15fde864..a7039a2a 100644 --- a/src/browserbase/_client.py +++ b/src/browserbase/_client.py @@ -19,10 +19,7 @@ ProxiesTypes, RequestOptions, ) -from ._utils import ( - is_given, - get_async_library, -) +from ._utils import is_given, get_async_library from ._version import __version__ from .resources import contexts, projects, extensions from ._streaming import Stream as Stream, AsyncStream as AsyncStream diff --git a/src/browserbase/resources/contexts.py b/src/browserbase/resources/contexts.py index 486cd5ff..0a438eda 100644 --- a/src/browserbase/resources/contexts.py +++ b/src/browserbase/resources/contexts.py @@ -6,10 +6,7 @@ from ..types import context_create_params from .._types import NOT_GIVEN, Body, Query, Headers, NotGiven -from .._utils import ( - maybe_transform, - async_maybe_transform, -) +from .._utils import maybe_transform, async_maybe_transform from .._compat import cached_property from .._resource import SyncAPIResource, AsyncAPIResource from .._response import ( diff --git a/src/browserbase/resources/extensions.py b/src/browserbase/resources/extensions.py index a98685a8..c7b0fae7 100644 --- a/src/browserbase/resources/extensions.py +++ b/src/browserbase/resources/extensions.py @@ -8,12 +8,7 @@ from ..types import extension_create_params from .._types import NOT_GIVEN, Body, Query, Headers, NoneType, NotGiven, FileTypes -from .._utils import ( - extract_files, - maybe_transform, - deepcopy_minimal, - async_maybe_transform, -) +from .._utils import extract_files, maybe_transform, deepcopy_minimal, async_maybe_transform from .._compat import cached_property from .._resource import SyncAPIResource, AsyncAPIResource from .._response import ( diff --git a/src/browserbase/resources/sessions/sessions.py b/src/browserbase/resources/sessions/sessions.py index 44814380..bf3314ad 100644 --- a/src/browserbase/resources/sessions/sessions.py +++ b/src/browserbase/resources/sessions/sessions.py @@ -25,10 +25,7 @@ AsyncUploadsResourceWithStreamingResponse, ) from ..._types import NOT_GIVEN, Body, Query, Headers, NotGiven -from ..._utils import ( - maybe_transform, - async_maybe_transform, -) +from ..._utils import maybe_transform, async_maybe_transform from ..._compat import cached_property from .downloads import ( DownloadsResource, diff --git a/src/browserbase/resources/sessions/uploads.py b/src/browserbase/resources/sessions/uploads.py index eed93499..69b6ccbe 100644 --- a/src/browserbase/resources/sessions/uploads.py +++ b/src/browserbase/resources/sessions/uploads.py @@ -7,12 +7,7 @@ import httpx from ..._types import NOT_GIVEN, Body, Query, Headers, NotGiven, FileTypes -from ..._utils import ( - extract_files, - maybe_transform, - deepcopy_minimal, - async_maybe_transform, -) +from ..._utils import extract_files, maybe_transform, deepcopy_minimal, async_maybe_transform from ..._compat import cached_property from ..._resource import SyncAPIResource, AsyncAPIResource from ..._response import ( From 1afc7f54c2a71c41d1ee3115149bb1cffaecb5ba Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 23 Apr 2025 03:16:52 +0000 Subject: [PATCH 124/330] chore(internal): fix list file params --- src/browserbase/_utils/_utils.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/browserbase/_utils/_utils.py b/src/browserbase/_utils/_utils.py index e5811bba..ea3cf3f2 100644 --- a/src/browserbase/_utils/_utils.py +++ b/src/browserbase/_utils/_utils.py @@ -72,8 +72,16 @@ def _extract_items( from .._files import assert_is_file_content # We have exhausted the path, return the entry we found. - assert_is_file_content(obj, key=flattened_key) 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 From 4de52e71b36ec8b807131181f7f2c02d31970021 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 23 Apr 2025 03:17:25 +0000 Subject: [PATCH 125/330] chore(internal): refactor retries to not use recursion --- src/browserbase/_base_client.py | 414 ++++++++++++++------------------ 1 file changed, 175 insertions(+), 239 deletions(-) diff --git a/src/browserbase/_base_client.py b/src/browserbase/_base_client.py index 20317b17..82e76c9d 100644 --- a/src/browserbase/_base_client.py +++ b/src/browserbase/_base_client.py @@ -437,8 +437,7 @@ def _build_headers(self, options: FinalRequestOptions, *, retries_taken: int = 0 headers = httpx.Headers(headers_dict) idempotency_header = self._idempotency_header - if idempotency_header and options.method.lower() != "get" and idempotency_header not in headers: - options.idempotency_key = options.idempotency_key or self._idempotency_key() + 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 @@ -903,7 +902,6 @@ def request( self, cast_to: Type[ResponseT], options: FinalRequestOptions, - remaining_retries: Optional[int] = None, *, stream: Literal[True], stream_cls: Type[_StreamT], @@ -914,7 +912,6 @@ def request( self, cast_to: Type[ResponseT], options: FinalRequestOptions, - remaining_retries: Optional[int] = None, *, stream: Literal[False] = False, ) -> ResponseT: ... @@ -924,7 +921,6 @@ def request( self, cast_to: Type[ResponseT], options: FinalRequestOptions, - remaining_retries: Optional[int] = None, *, stream: bool = False, stream_cls: Type[_StreamT] | None = None, @@ -934,125 +930,109 @@ def request( self, cast_to: Type[ResponseT], options: FinalRequestOptions, - remaining_retries: Optional[int] = None, *, stream: bool = False, stream_cls: type[_StreamT] | None = None, ) -> ResponseT | _StreamT: - if remaining_retries is not None: - retries_taken = options.get_max_retries(self.max_retries) - remaining_retries - else: - retries_taken = 0 - - return self._request( - cast_to=cast_to, - options=options, - stream=stream, - stream_cls=stream_cls, - retries_taken=retries_taken, - ) + cast_to = self._maybe_override_cast_to(cast_to, options) - def _request( - self, - *, - cast_to: Type[ResponseT], - options: FinalRequestOptions, - retries_taken: int, - stream: bool, - stream_cls: type[_StreamT] | None, - ) -> ResponseT | _StreamT: # 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) - - cast_to = self._maybe_override_cast_to(cast_to, options) - options = self._prepare_options(options) - - remaining_retries = options.get_max_retries(self.max_retries) - retries_taken - request = self._build_request(options, retries_taken=retries_taken) - self._prepare_request(request) - - if options.idempotency_key: + 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 = options.idempotency_key + input_options.idempotency_key = self._idempotency_key() - kwargs: HttpxSendArgs = {} - if self.custom_auth is not None: - kwargs["auth"] = self.custom_auth + response: httpx.Response | None = None + max_retries = input_options.get_max_retries(self.max_retries) - log.debug("Sending HTTP Request: %s %s", request.method, request.url) + retries_taken = 0 + for retries_taken in range(max_retries + 1): + options = model_copy(input_options) + options = self._prepare_options(options) - 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) + remaining_retries = max_retries - retries_taken + request = self._build_request(options, retries_taken=retries_taken) + self._prepare_request(request) - if remaining_retries > 0: - return self._retry_request( - input_options, - cast_to, - retries_taken=retries_taken, - stream=stream, - stream_cls=stream_cls, - response_headers=None, - ) + kwargs: HttpxSendArgs = {} + if self.custom_auth is not None: + kwargs["auth"] = self.custom_auth - log.debug("Raising timeout error") - raise APITimeoutError(request=request) from err - except Exception as err: - log.debug("Encountered Exception", exc_info=True) + log.debug("Sending HTTP Request: %s %s", request.method, request.url) - if remaining_retries > 0: - return self._retry_request( - input_options, - cast_to, - retries_taken=retries_taken, - stream=stream, - stream_cls=stream_cls, - response_headers=None, + 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, + ) - 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 - 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() - return self._retry_request( - input_options, - cast_to, - retries_taken=retries_taken, - response_headers=err.response.headers, - stream=stream, - stream_cls=stream_cls, - ) + # 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() - # 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 - 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, @@ -1062,37 +1042,20 @@ def _request( retries_taken=retries_taken, ) - def _retry_request( - self, - options: FinalRequestOptions, - cast_to: Type[ResponseT], - *, - retries_taken: int, - response_headers: httpx.Headers | None, - stream: bool, - stream_cls: type[_StreamT] | None, - ) -> ResponseT | _StreamT: - remaining_retries = options.get_max_retries(self.max_retries) - 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) + 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) - # In a synchronous context we are blocking the entire thread. Up to the library user to run the client in a - # different thread if necessary. time.sleep(timeout) - return self._request( - options=options, - cast_to=cast_to, - retries_taken=retries_taken + 1, - stream=stream, - stream_cls=stream_cls, - ) - def _process_response( self, *, @@ -1436,7 +1399,6 @@ async def request( options: FinalRequestOptions, *, stream: Literal[False] = False, - remaining_retries: Optional[int] = None, ) -> ResponseT: ... @overload @@ -1447,7 +1409,6 @@ async def request( *, stream: Literal[True], stream_cls: type[_AsyncStreamT], - remaining_retries: Optional[int] = None, ) -> _AsyncStreamT: ... @overload @@ -1458,7 +1419,6 @@ async def request( *, stream: bool, stream_cls: type[_AsyncStreamT] | None = None, - remaining_retries: Optional[int] = None, ) -> ResponseT | _AsyncStreamT: ... async def request( @@ -1468,120 +1428,111 @@ async def request( *, stream: bool = False, stream_cls: type[_AsyncStreamT] | None = None, - remaining_retries: Optional[int] = None, - ) -> ResponseT | _AsyncStreamT: - if remaining_retries is not None: - retries_taken = options.get_max_retries(self.max_retries) - remaining_retries - else: - retries_taken = 0 - - return await self._request( - cast_to=cast_to, - options=options, - stream=stream, - stream_cls=stream_cls, - retries_taken=retries_taken, - ) - - async def _request( - self, - cast_to: Type[ResponseT], - options: FinalRequestOptions, - *, - stream: bool, - stream_cls: type[_AsyncStreamT] | None, - retries_taken: int, ) -> 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) - - cast_to = self._maybe_override_cast_to(cast_to, options) - options = await self._prepare_options(options) - - remaining_retries = options.get_max_retries(self.max_retries) - retries_taken - request = self._build_request(options, retries_taken=retries_taken) - await self._prepare_request(request) - - if options.idempotency_key: + 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 = options.idempotency_key + input_options.idempotency_key = self._idempotency_key() - kwargs: HttpxSendArgs = {} - if self.custom_auth is not None: - kwargs["auth"] = self.custom_auth + response: httpx.Response | None = None + max_retries = input_options.get_max_retries(self.max_retries) - 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) + retries_taken = 0 + for retries_taken in range(max_retries + 1): + options = model_copy(input_options) + options = await self._prepare_options(options) - if remaining_retries > 0: - return await self._retry_request( - input_options, - cast_to, - retries_taken=retries_taken, - stream=stream, - stream_cls=stream_cls, - response_headers=None, - ) + remaining_retries = max_retries - retries_taken + request = self._build_request(options, retries_taken=retries_taken) + await self._prepare_request(request) - log.debug("Raising timeout error") - raise APITimeoutError(request=request) from err - except Exception as err: - log.debug("Encountered Exception", exc_info=True) + kwargs: HttpxSendArgs = {} + if self.custom_auth is not None: + kwargs["auth"] = self.custom_auth - if remaining_retries > 0: - return await self._retry_request( - input_options, - cast_to, - retries_taken=retries_taken, - stream=stream, - stream_cls=stream_cls, - response_headers=None, - ) + log.debug("Sending HTTP Request: %s %s", request.method, request.url) - log.debug("Raising connection error") - raise APIConnectionError(request=request) from err + 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, + ) - log.debug( - 'HTTP Request: %s %s "%i %s"', request.method, request.url, response.status_code, response.reason_phrase - ) + 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 - 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() - return await self._retry_request( - input_options, - cast_to, - retries_taken=retries_taken, - response_headers=err.response.headers, - stream=stream, - stream_cls=stream_cls, - ) + # 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() - # 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 - 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, @@ -1591,35 +1542,20 @@ async def _request( retries_taken=retries_taken, ) - async def _retry_request( - self, - options: FinalRequestOptions, - cast_to: Type[ResponseT], - *, - retries_taken: int, - response_headers: httpx.Headers | None, - stream: bool, - stream_cls: type[_AsyncStreamT] | None, - ) -> ResponseT | _AsyncStreamT: - remaining_retries = options.get_max_retries(self.max_retries) - 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) + 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) - return await self._request( - options=options, - cast_to=cast_to, - retries_taken=retries_taken + 1, - stream=stream, - stream_cls=stream_cls, - ) - async def _process_response( self, *, From ac7b65cbecbfd70b5d960329e3415ddb745493e8 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 23 Apr 2025 03:17:47 +0000 Subject: [PATCH 126/330] fix(pydantic v1): more robust ModelField.annotation check --- src/browserbase/_models.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/browserbase/_models.py b/src/browserbase/_models.py index 58b9263e..798956f1 100644 --- a/src/browserbase/_models.py +++ b/src/browserbase/_models.py @@ -626,8 +626,8 @@ def _build_discriminated_union_meta(*, union: type, meta_annotations: tuple[Any, # Note: if one variant defines an alias then they all should discriminator_alias = field_info.alias - if field_info.annotation and is_literal_type(field_info.annotation): - for entry in get_args(field_info.annotation): + 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 From 9787b5f66d34967ef12fe792b14598f4b6bdfb9a Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 24 Apr 2025 02:29:24 +0000 Subject: [PATCH 127/330] chore(internal): minor formatting changes --- src/browserbase/types/context_create_response.py | 1 - src/browserbase/types/context_update_response.py | 1 - src/browserbase/types/project_usage.py | 1 - src/browserbase/types/sessions/upload_create_response.py | 1 - 4 files changed, 4 deletions(-) diff --git a/src/browserbase/types/context_create_response.py b/src/browserbase/types/context_create_response.py index c168596e..8e2f7aa3 100644 --- a/src/browserbase/types/context_create_response.py +++ b/src/browserbase/types/context_create_response.py @@ -1,6 +1,5 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - from pydantic import Field as FieldInfo from .._models import BaseModel diff --git a/src/browserbase/types/context_update_response.py b/src/browserbase/types/context_update_response.py index d07e50e7..7e16c624 100644 --- a/src/browserbase/types/context_update_response.py +++ b/src/browserbase/types/context_update_response.py @@ -1,6 +1,5 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - from pydantic import Field as FieldInfo from .._models import BaseModel diff --git a/src/browserbase/types/project_usage.py b/src/browserbase/types/project_usage.py index f68cc2da..c8a03f5b 100644 --- a/src/browserbase/types/project_usage.py +++ b/src/browserbase/types/project_usage.py @@ -1,6 +1,5 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - from pydantic import Field as FieldInfo from .._models import BaseModel diff --git a/src/browserbase/types/sessions/upload_create_response.py b/src/browserbase/types/sessions/upload_create_response.py index ceece2cd..abeed017 100644 --- a/src/browserbase/types/sessions/upload_create_response.py +++ b/src/browserbase/types/sessions/upload_create_response.py @@ -1,6 +1,5 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - from ..._models import BaseModel __all__ = ["UploadCreateResponse"] From ab3cafaf5847a54913e3b5202acc4241eb7e5c12 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 24 Apr 2025 02:29:59 +0000 Subject: [PATCH 128/330] chore(internal): codegen related update --- .github/workflows/ci.yml | 16 ++++++++-------- .github/workflows/publish-pypi.yml | 2 +- .github/workflows/release-doctor.yml | 2 +- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 04b083ca..33820422 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,18 +1,18 @@ name: CI on: push: - branches: - - main - pull_request: - branches: - - main - - next + branches-ignore: + - 'generated' + - 'codegen/**' + - 'integrated/**' + - 'stl-preview-head/**' + - 'stl-preview-base/**' jobs: lint: timeout-minutes: 10 name: lint - runs-on: ubuntu-latest + runs-on: depot-ubuntu-24.04 steps: - uses: actions/checkout@v4 @@ -33,7 +33,7 @@ jobs: test: timeout-minutes: 10 name: test - runs-on: ubuntu-latest + runs-on: depot-ubuntu-24.04 steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/publish-pypi.yml b/.github/workflows/publish-pypi.yml index b3c832c7..05177abb 100644 --- a/.github/workflows/publish-pypi.yml +++ b/.github/workflows/publish-pypi.yml @@ -11,7 +11,7 @@ on: jobs: publish: name: publish - runs-on: ubuntu-latest + runs-on: depot-ubuntu-24.04 steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/release-doctor.yml b/.github/workflows/release-doctor.yml index 3e17e458..8d5b2590 100644 --- a/.github/workflows/release-doctor.yml +++ b/.github/workflows/release-doctor.yml @@ -8,7 +8,7 @@ on: jobs: release_doctor: name: release doctor - runs-on: ubuntu-latest + runs-on: depot-ubuntu-24.04 if: github.repository == 'browserbase/sdk-python' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch' || startsWith(github.head_ref, 'release-please') || github.head_ref == 'next') steps: From 1c05ef966762c83e4c120f258a48de7263e43eb7 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 24 Apr 2025 02:30:33 +0000 Subject: [PATCH 129/330] chore(ci): only use depot for staging repos --- .github/workflows/ci.yml | 4 ++-- .github/workflows/publish-pypi.yml | 2 +- .github/workflows/release-doctor.yml | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 33820422..6a27bef1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,7 +12,7 @@ jobs: lint: timeout-minutes: 10 name: lint - runs-on: depot-ubuntu-24.04 + runs-on: ${{ github.repository == 'stainless-sdks/browserbase-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} steps: - uses: actions/checkout@v4 @@ -33,7 +33,7 @@ jobs: test: timeout-minutes: 10 name: test - runs-on: depot-ubuntu-24.04 + runs-on: ${{ github.repository == 'stainless-sdks/browserbase-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/publish-pypi.yml b/.github/workflows/publish-pypi.yml index 05177abb..b3c832c7 100644 --- a/.github/workflows/publish-pypi.yml +++ b/.github/workflows/publish-pypi.yml @@ -11,7 +11,7 @@ on: jobs: publish: name: publish - runs-on: depot-ubuntu-24.04 + runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/release-doctor.yml b/.github/workflows/release-doctor.yml index 8d5b2590..3e17e458 100644 --- a/.github/workflows/release-doctor.yml +++ b/.github/workflows/release-doctor.yml @@ -8,7 +8,7 @@ on: jobs: release_doctor: name: release doctor - runs-on: depot-ubuntu-24.04 + runs-on: ubuntu-latest if: github.repository == 'browserbase/sdk-python' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch' || startsWith(github.head_ref, 'release-please') || github.head_ref == 'next') steps: From 16ec655bdebdb9465fef122c4deb4dd7f4735d89 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 24 Apr 2025 02:31:57 +0000 Subject: [PATCH 130/330] chore: broadly detect json family of content-type headers --- src/browserbase/_response.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/browserbase/_response.py b/src/browserbase/_response.py index e79cb15f..5f8d0f48 100644 --- a/src/browserbase/_response.py +++ b/src/browserbase/_response.py @@ -235,7 +235,7 @@ def _parse(self, *, to: type[_T] | None = None) -> R | _T: # 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 content_type != "application/json": + if not content_type.endswith("json"): if is_basemodel(cast_to): try: data = response.json() From 974408b3b5eda7c14bdf1306d483d759fdd54b27 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 8 May 2025 18:23:37 +0000 Subject: [PATCH 131/330] chore(internal): version bump --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/browserbase/_version.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index d0ab6645..2a8f4ffd 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "1.2.0" + ".": "1.3.0" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index e70dab6b..c3638640 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "browserbase" -version = "1.2.0" +version = "1.3.0" description = "The official Python library for the Browserbase API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/browserbase/_version.py b/src/browserbase/_version.py index 4207df36..9c23d1bb 100644 --- a/src/browserbase/_version.py +++ b/src/browserbase/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "browserbase" -__version__ = "1.2.0" # x-release-please-version +__version__ = "1.3.0" # x-release-please-version From 0f2c8a9380c17feb79f19d27458c2cbeb62891cc Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 9 May 2025 03:53:45 +0000 Subject: [PATCH 132/330] chore(internal): avoid errors for isinstance checks on proxies --- src/browserbase/_utils/_proxy.py | 5 ++++- tests/test_utils/test_proxy.py | 11 +++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/browserbase/_utils/_proxy.py b/src/browserbase/_utils/_proxy.py index ffd883e9..0f239a33 100644 --- a/src/browserbase/_utils/_proxy.py +++ b/src/browserbase/_utils/_proxy.py @@ -46,7 +46,10 @@ def __dir__(self) -> Iterable[str]: @property # type: ignore @override def __class__(self) -> type: # pyright: ignore - proxied = self.__get_proxied__() + try: + proxied = self.__get_proxied__() + except Exception: + return type(self) if issubclass(type(proxied), LazyProxy): return type(proxied) return proxied.__class__ diff --git a/tests/test_utils/test_proxy.py b/tests/test_utils/test_proxy.py index 986bef9d..d4e2f311 100644 --- a/tests/test_utils/test_proxy.py +++ b/tests/test_utils/test_proxy.py @@ -21,3 +21,14 @@ def test_recursive_proxy() -> None: 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) From 0a4e24e965d14d583d952bd51654a1d8c62dc932 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 10 May 2025 03:23:09 +0000 Subject: [PATCH 133/330] fix(package): support direct resource imports --- src/browserbase/__init__.py | 5 +++++ src/browserbase/_utils/_resources_proxy.py | 24 ++++++++++++++++++++++ 2 files changed, 29 insertions(+) create mode 100644 src/browserbase/_utils/_resources_proxy.py diff --git a/src/browserbase/__init__.py b/src/browserbase/__init__.py index ce50c4ea..e954b0ee 100644 --- a/src/browserbase/__init__.py +++ b/src/browserbase/__init__.py @@ -1,5 +1,7 @@ # 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 from ._utils import file_from_path @@ -78,6 +80,9 @@ "DefaultAsyncHttpxClient", ] +if not _t.TYPE_CHECKING: + from ._utils._resources_proxy import resources as resources + _setup_logging() # Update the __module__ attribute for exported symbols so that diff --git a/src/browserbase/_utils/_resources_proxy.py b/src/browserbase/_utils/_resources_proxy.py new file mode 100644 index 00000000..3901271d --- /dev/null +++ b/src/browserbase/_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 `browserbase.resources` module. + + This is used so that we can lazily import `browserbase.resources` only when + needed *and* so that users can just import `browserbase` and reference `browserbase.resources` + """ + + @override + def __load__(self) -> Any: + import importlib + + mod = importlib.import_module("browserbase.resources") + return mod + + +resources = ResourcesProxy().__as_proxied__() From a1b704d18e68b7866a074762876dbd8f6b3948a6 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 15 May 2025 04:30:43 +0000 Subject: [PATCH 134/330] chore(ci): upload sdks to package manager --- .github/workflows/ci.yml | 24 ++++++++++++++++++++++++ scripts/utils/upload-artifact.sh | 25 +++++++++++++++++++++++++ 2 files changed, 49 insertions(+) create mode 100755 scripts/utils/upload-artifact.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6a27bef1..1f415886 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,6 +30,30 @@ jobs: - name: Run lints run: ./scripts/lint + upload: + if: github.repository == 'stainless-sdks/browserbase-python' + timeout-minutes: 10 + name: upload + permissions: + contents: read + id-token: write + runs-on: depot-ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + + - name: Get GitHub OIDC Token + id: github-oidc + uses: actions/github-script@v6 + with: + script: core.setOutput('github_token', await core.getIDToken()); + + - name: Upload tarball + 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 diff --git a/scripts/utils/upload-artifact.sh b/scripts/utils/upload-artifact.sh new file mode 100755 index 00000000..c3d0e454 --- /dev/null +++ b/scripts/utils/upload-artifact.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env bash +set -exuo pipefail + +RESPONSE=$(curl -X POST "$URL" \ + -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=$(tar -cz . | curl -v -X PUT \ + -H "Content-Type: application/gzip" \ + --data-binary @- "$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: npm install 'https://pkg.stainless.com/s/browserbase-python/$SHA'\033[0m" +else + echo -e "\033[31mFailed to upload artifact.\033[0m" + exit 1 +fi From 83aed8d4c0338e3eec6aeac630e022805b3cf8da Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 16 May 2025 03:35:19 +0000 Subject: [PATCH 135/330] chore(ci): fix installation instructions --- scripts/utils/upload-artifact.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/utils/upload-artifact.sh b/scripts/utils/upload-artifact.sh index c3d0e454..7c3d028a 100755 --- a/scripts/utils/upload-artifact.sh +++ b/scripts/utils/upload-artifact.sh @@ -18,7 +18,7 @@ UPLOAD_RESPONSE=$(tar -cz . | curl -v -X PUT \ 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: npm install 'https://pkg.stainless.com/s/browserbase-python/$SHA'\033[0m" + echo -e "\033[32mInstallation: pip install 'https://pkg.stainless.com/s/browserbase-python/$SHA'\033[0m" else echo -e "\033[31mFailed to upload artifact.\033[0m" exit 1 From 665e3b6b03578ca839bfa88c12f9d54ad8bf5ef7 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 16 May 2025 19:18:25 +0000 Subject: [PATCH 136/330] feat(api): api update --- .stats.yml | 4 ++-- README.md | 2 ++ src/browserbase/types/session_create_params.py | 12 ++++++++++++ tests/api_resources/test_sessions.py | 4 ++++ 4 files changed, 20 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index 92128df9..38c95a82 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 18 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fbrowserbase-45328621800082e652c9b2f34b176b89ebba3af423ea9f4fed91a359cf4e0ae4.yml -openapi_spec_hash: c20658b49312b14d158ce5c88f34ee34 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fbrowserbase-e2ed1b5267eeff92982918505349017b9155da2c7ab948787ab11cf9068af1b8.yml +openapi_spec_hash: 6639c21dccb52ca610cae833227a9791 config_hash: 74882e23a455dece33e43a27e67f0fbb diff --git a/README.md b/README.md index c99b365c..c58f2232 100644 --- a/README.md +++ b/README.md @@ -91,6 +91,8 @@ session = client.sessions.create( browser_settings={ "advanced_stealth": True, "block_ads": True, + "captcha_image_selector": "captchaImageSelector", + "captcha_input_selector": "captchaInputSelector", "context": { "id": "id", "persist": True, diff --git a/src/browserbase/types/session_create_params.py b/src/browserbase/types/session_create_params.py index f3b9606d..1f5324f8 100644 --- a/src/browserbase/types/session_create_params.py +++ b/src/browserbase/types/session_create_params.py @@ -120,6 +120,18 @@ class BrowserSettings(TypedDict, total=False): block_ads: Annotated[bool, PropertyInfo(alias="blockAds")] """Enable or disable ad blocking in the browser. Defaults to `false`.""" + captcha_image_selector: Annotated[str, PropertyInfo(alias="captchaImageSelector")] + """Custom selector for captcha image. + + See [Custom Captcha Solving](/features/stealth-mode#custom-captcha-solving) + """ + + captcha_input_selector: Annotated[str, PropertyInfo(alias="captchaInputSelector")] + """Custom selector for captcha input. + + See [Custom Captcha Solving](/features/stealth-mode#custom-captcha-solving) + """ + context: BrowserSettingsContext extension_id: Annotated[str, PropertyInfo(alias="extensionId")] diff --git a/tests/api_resources/test_sessions.py b/tests/api_resources/test_sessions.py index 0581655c..4a17c4f6 100644 --- a/tests/api_resources/test_sessions.py +++ b/tests/api_resources/test_sessions.py @@ -37,6 +37,8 @@ def test_method_create_with_all_params(self, client: Browserbase) -> None: browser_settings={ "advanced_stealth": True, "block_ads": True, + "captcha_image_selector": "captchaImageSelector", + "captcha_input_selector": "captchaInputSelector", "context": { "id": "id", "persist": True, @@ -269,6 +271,8 @@ async def test_method_create_with_all_params(self, async_client: AsyncBrowserbas browser_settings={ "advanced_stealth": True, "block_ads": True, + "captcha_image_selector": "captchaImageSelector", + "captcha_input_selector": "captchaInputSelector", "context": { "id": "id", "persist": True, From 8291a25923190e07863afc4395dd102f57d63477 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 16 May 2025 20:50:38 +0000 Subject: [PATCH 137/330] chore(internal): version bump --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/browserbase/_version.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 2a8f4ffd..3e9af1b3 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "1.3.0" + ".": "1.4.0" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index c3638640..77a3b397 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "browserbase" -version = "1.3.0" +version = "1.4.0" description = "The official Python library for the Browserbase API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/browserbase/_version.py b/src/browserbase/_version.py index 9c23d1bb..3c0492ea 100644 --- a/src/browserbase/_version.py +++ b/src/browserbase/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "browserbase" -__version__ = "1.3.0" # x-release-please-version +__version__ = "1.4.0" # x-release-please-version From 489755558f3696253bf12de7b3cc61f28661a934 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 22 May 2025 02:35:16 +0000 Subject: [PATCH 138/330] chore(docs): grammar improvements --- SECURITY.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/SECURITY.md b/SECURITY.md index e10eb19a..ad64e4b9 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -16,11 +16,11 @@ 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 Browserbase, please follow the respective company's security reporting guidelines. ### Browserbase Terms and Policies -Please contact support@browserbase.com for any questions or concerns regarding security of our services. +Please contact support@browserbase.com for any questions or concerns regarding the security of our services. --- From 86b54e119032b8625c3a576a01ae1af815eb1569 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 3 Jun 2025 02:27:48 +0000 Subject: [PATCH 139/330] chore(docs): remove reference to rye shell --- CONTRIBUTING.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 45a7298e..5f8bfea6 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -17,8 +17,7 @@ $ rye sync --all-features You can then run scripts using `rye run python script.py` or by activating the virtual environment: ```sh -$ rye shell -# or manually activate - https://docs.python.org/3/library/venv.html#how-venvs-work +# 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 From ba0a4839e5052390ea051ec1a411498a421acae1 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 3 Jun 2025 02:40:00 +0000 Subject: [PATCH 140/330] chore(docs): remove unnecessary param examples --- README.md | 32 +------------------------------- 1 file changed, 1 insertion(+), 31 deletions(-) diff --git a/README.md b/README.md index c58f2232..73bf10a8 100644 --- a/README.md +++ b/README.md @@ -88,37 +88,7 @@ client = Browserbase() session = client.sessions.create( project_id="projectId", - browser_settings={ - "advanced_stealth": True, - "block_ads": True, - "captcha_image_selector": "captchaImageSelector", - "captcha_input_selector": "captchaInputSelector", - "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, - }, - }, + browser_settings={}, ) print(session.browser_settings) ``` From 452a7c736496883adfca6d0a6eba1c1151ee65f8 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 3 Jun 2025 03:43:29 +0000 Subject: [PATCH 141/330] feat(client): add follow_redirects request option --- src/browserbase/_base_client.py | 6 ++++ src/browserbase/_models.py | 2 ++ src/browserbase/_types.py | 2 ++ tests/test_client.py | 54 +++++++++++++++++++++++++++++++++ 4 files changed, 64 insertions(+) diff --git a/src/browserbase/_base_client.py b/src/browserbase/_base_client.py index 82e76c9d..587ec4d3 100644 --- a/src/browserbase/_base_client.py +++ b/src/browserbase/_base_client.py @@ -960,6 +960,9 @@ def request( 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 @@ -1460,6 +1463,9 @@ async def request( 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 diff --git a/src/browserbase/_models.py b/src/browserbase/_models.py index 798956f1..4f214980 100644 --- a/src/browserbase/_models.py +++ b/src/browserbase/_models.py @@ -737,6 +737,7 @@ class FinalRequestOptionsInput(TypedDict, total=False): idempotency_key: str json_data: Body extra_json: AnyMapping + follow_redirects: bool @final @@ -750,6 +751,7 @@ class FinalRequestOptions(pydantic.BaseModel): 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. diff --git a/src/browserbase/_types.py b/src/browserbase/_types.py index a8833dce..b07c0e14 100644 --- a/src/browserbase/_types.py +++ b/src/browserbase/_types.py @@ -100,6 +100,7 @@ class RequestOptions(TypedDict, total=False): params: Query extra_json: AnyMapping idempotency_key: str + follow_redirects: bool # Sentinel class used until PEP 0661 is accepted @@ -215,3 +216,4 @@ class _GenericAlias(Protocol): class HttpxSendArgs(TypedDict, total=False): auth: httpx.Auth + follow_redirects: bool diff --git a/tests/test_client.py b/tests/test_client.py index d03654df..69ff3ba3 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -832,6 +832,33 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: assert response.http_request.headers.get("x-stainless-retry-count") == "42" + @pytest.mark.respx(base_url=base_url) + def test_follow_redirects(self, respx_mock: MockRouter) -> 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 = self.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) -> 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: + self.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 TestAsyncBrowserbase: client = AsyncBrowserbase(base_url=base_url, api_key=api_key, _strict_response_validation=True) @@ -1659,3 +1686,30 @@ async def test_main() -> None: raise AssertionError("calling get_platform using asyncify resulted in a hung process") time.sleep(0.1) + + @pytest.mark.respx(base_url=base_url) + async def test_follow_redirects(self, respx_mock: MockRouter) -> 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 self.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) -> 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 self.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" From 2ab72604382c6839b9e0ee086b95313994713e9a Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 3 Jun 2025 23:30:43 +0000 Subject: [PATCH 142/330] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index 38c95a82..8cc23115 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 18 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fbrowserbase-e2ed1b5267eeff92982918505349017b9155da2c7ab948787ab11cf9068af1b8.yml openapi_spec_hash: 6639c21dccb52ca610cae833227a9791 -config_hash: 74882e23a455dece33e43a27e67f0fbb +config_hash: b3ca4ec5b02e5333af51ebc2e9fdef1b From e847624c6bb320070c62359c9505b9577c40e73d Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 13 Jun 2025 02:13:59 +0000 Subject: [PATCH 143/330] chore(tests): run tests in parallel --- pyproject.toml | 3 ++- requirements-dev.lock | 4 ++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 77a3b397..c8d76a5b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -54,6 +54,7 @@ dev-dependencies = [ "importlib-metadata>=6.7.0", "rich>=13.7.1", "nest_asyncio==1.6.0", + "pytest-xdist>=3.6.1", ] [tool.rye.scripts] @@ -125,7 +126,7 @@ replacement = '[\1](https://github.com/browserbase/sdk-python/tree/main/\g<2>)' [tool.pytest.ini_options] testpaths = ["tests"] -addopts = "--tb=short" +addopts = "--tb=short -n auto" xfail_strict = true asyncio_mode = "auto" asyncio_default_fixture_loop_scope = "session" diff --git a/requirements-dev.lock b/requirements-dev.lock index a26bd10f..cc5b61a2 100644 --- a/requirements-dev.lock +++ b/requirements-dev.lock @@ -30,6 +30,8 @@ distro==1.8.0 exceptiongroup==1.2.2 # via anyio # via pytest +execnet==2.1.1 + # via pytest-xdist filelock==3.12.4 # via virtualenv h11==0.14.0 @@ -72,7 +74,9 @@ pygments==2.18.0 pyright==1.1.399 pytest==8.3.3 # via pytest-asyncio + # via pytest-xdist pytest-asyncio==0.24.0 +pytest-xdist==3.7.0 python-dateutil==2.8.2 # via time-machine pytz==2023.3.post1 From d0b8b5efb099fa6760c4c0dcb31283424e4fd060 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 13 Jun 2025 02:38:37 +0000 Subject: [PATCH 144/330] fix(client): correctly parse binary response | stream --- src/browserbase/_base_client.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/src/browserbase/_base_client.py b/src/browserbase/_base_client.py index 587ec4d3..9b8dc52a 100644 --- a/src/browserbase/_base_client.py +++ b/src/browserbase/_base_client.py @@ -1071,7 +1071,14 @@ def _process_response( ) -> ResponseT: origin = get_origin(cast_to) or cast_to - if inspect.isclass(origin) and issubclass(origin, BaseAPIResponse): + 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}") @@ -1574,7 +1581,14 @@ async def _process_response( ) -> ResponseT: origin = get_origin(cast_to) or cast_to - if inspect.isclass(origin) and issubclass(origin, BaseAPIResponse): + 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}") From e17ea601b0b4bdaa9ace8b6fd8004d8450bff6e5 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 17 Jun 2025 02:44:52 +0000 Subject: [PATCH 145/330] chore(tests): add tests for httpx client instantiation & proxies --- tests/test_client.py | 46 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/tests/test_client.py b/tests/test_client.py index 69ff3ba3..8485f8fc 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -31,6 +31,8 @@ DEFAULT_TIMEOUT, HTTPX_DEFAULT_TIMEOUT, BaseClient, + DefaultHttpxClient, + DefaultAsyncHttpxClient, make_request_options, ) from browserbase.types.session_create_params import SessionCreateParams @@ -832,6 +834,28 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: 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) -> None: # Test that the default follow_redirects=True allows following redirects @@ -1687,6 +1711,28 @@ async def test_main() -> None: time.sleep(0.1) + 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) -> None: # Test that the default follow_redirects=True allows following redirects From dee68e633358503c1ccf25e7cf75fbc284f573d5 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 17 Jun 2025 04:13:40 +0000 Subject: [PATCH 146/330] chore(internal): update conftest.py --- tests/conftest.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/conftest.py b/tests/conftest.py index 94b8e723..b81e3b2d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,3 +1,5 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + from __future__ import annotations import os From 99089febff736edf410b4ab7e9afe790aa0e5780 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 17 Jun 2025 06:44:19 +0000 Subject: [PATCH 147/330] chore(ci): enable for pull requests --- .github/workflows/ci.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1f415886..4946ac10 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,6 +7,10 @@ on: - 'integrated/**' - 'stl-preview-head/**' - 'stl-preview-base/**' + pull_request: + branches-ignore: + - 'stl-preview-head/**' + - 'stl-preview-base/**' jobs: lint: From 6357f3383fcc8446a723188ea855f0cf2eab76fa Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 18 Jun 2025 02:17:06 +0000 Subject: [PATCH 148/330] chore(readme): update badges --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 73bf10a8..c3c1a6ec 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Browserbase Python API library -[![PyPI version](https://img.shields.io/pypi/v/browserbase.svg)](https://pypi.org/project/browserbase/) +[![PyPI version]()](https://pypi.org/project/browserbase/) The Browserbase Python library provides convenient access to the Browserbase REST API from any Python 3.8+ application. The library includes type definitions for all request params and response fields, From 618e98714a003bab30465f72b101343cd1ceff86 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 18 Jun 2025 05:53:08 +0000 Subject: [PATCH 149/330] fix(tests): fix: tests which call HTTP endpoints directly with the example parameters --- tests/test_client.py | 45 ++++++++++++-------------------------------- 1 file changed, 12 insertions(+), 33 deletions(-) diff --git a/tests/test_client.py b/tests/test_client.py index 8485f8fc..4c9938b1 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -23,9 +23,7 @@ from browserbase import Browserbase, AsyncBrowserbase, APIResponseValidationError from browserbase._types import Omit -from browserbase._utils import maybe_transform from browserbase._models import BaseModel, FinalRequestOptions -from browserbase._constants import RAW_RESPONSE_HEADER from browserbase._exceptions import APIStatusError, APITimeoutError, BrowserbaseError, APIResponseValidationError from browserbase._base_client import ( DEFAULT_TIMEOUT, @@ -35,7 +33,6 @@ DefaultAsyncHttpxClient, make_request_options, ) -from browserbase.types.session_create_params import SessionCreateParams from .utils import update_env @@ -725,32 +722,21 @@ def test_parse_retry_after_header(self, remaining_retries: int, retry_after: str @mock.patch("browserbase._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) -> None: + def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter, client: Browserbase) -> None: respx_mock.post("/v1/sessions").mock(side_effect=httpx.TimeoutException("Test timeout error")) with pytest.raises(APITimeoutError): - self.client.post( - "/v1/sessions", - body=cast(object, maybe_transform(dict(project_id="your_project_id"), SessionCreateParams)), - cast_to=httpx.Response, - options={"headers": {RAW_RESPONSE_HEADER: "stream"}}, - ) + client.sessions.with_streaming_response.create(project_id="projectId").__enter__() assert _get_open_connections(self.client) == 0 @mock.patch("browserbase._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) -> None: + def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter, client: Browserbase) -> None: respx_mock.post("/v1/sessions").mock(return_value=httpx.Response(500)) with pytest.raises(APIStatusError): - self.client.post( - "/v1/sessions", - body=cast(object, maybe_transform(dict(project_id="your_project_id"), SessionCreateParams)), - cast_to=httpx.Response, - options={"headers": {RAW_RESPONSE_HEADER: "stream"}}, - ) - + client.sessions.with_streaming_response.create(project_id="projectId").__enter__() assert _get_open_connections(self.client) == 0 @pytest.mark.parametrize("failures_before_success", [0, 2, 4]) @@ -1554,32 +1540,25 @@ async def test_parse_retry_after_header(self, remaining_retries: int, retry_afte @mock.patch("browserbase._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) -> None: + async def test_retrying_timeout_errors_doesnt_leak( + self, respx_mock: MockRouter, async_client: AsyncBrowserbase + ) -> None: respx_mock.post("/v1/sessions").mock(side_effect=httpx.TimeoutException("Test timeout error")) with pytest.raises(APITimeoutError): - await self.client.post( - "/v1/sessions", - body=cast(object, maybe_transform(dict(project_id="your_project_id"), SessionCreateParams)), - cast_to=httpx.Response, - options={"headers": {RAW_RESPONSE_HEADER: "stream"}}, - ) + await async_client.sessions.with_streaming_response.create(project_id="projectId").__aenter__() assert _get_open_connections(self.client) == 0 @mock.patch("browserbase._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) -> None: + async def test_retrying_status_errors_doesnt_leak( + self, respx_mock: MockRouter, async_client: AsyncBrowserbase + ) -> None: respx_mock.post("/v1/sessions").mock(return_value=httpx.Response(500)) with pytest.raises(APIStatusError): - await self.client.post( - "/v1/sessions", - body=cast(object, maybe_transform(dict(project_id="your_project_id"), SessionCreateParams)), - cast_to=httpx.Response, - options={"headers": {RAW_RESPONSE_HEADER: "stream"}}, - ) - + await async_client.sessions.with_streaming_response.create(project_id="projectId").__aenter__() assert _get_open_connections(self.client) == 0 @pytest.mark.parametrize("failures_before_success", [0, 2, 4]) From 862feb3260ba5e9dc0cc487e239f7032eb51a8b3 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 19 Jun 2025 02:55:23 +0000 Subject: [PATCH 150/330] docs(client): fix httpx.Timeout documentation reference --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c3c1a6ec..2d1471df 100644 --- a/README.md +++ b/README.md @@ -179,7 +179,7 @@ client.with_options(max_retries=5).sessions.create( ### 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/#fine-tuning-the-configuration) object: +which accepts a float or an [`httpx.Timeout`](https://www.python-httpx.org/advanced/timeouts/#fine-tuning-the-configuration) object: ```python from browserbase import Browserbase From 34cbb364203c9e6e5e7b10cb28a7d3e627370f52 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 21 Jun 2025 04:52:11 +0000 Subject: [PATCH 151/330] feat(client): add support for aiohttp --- README.md | 34 +++++++++++++++ pyproject.toml | 2 + requirements-dev.lock | 27 ++++++++++++ requirements.lock | 27 ++++++++++++ src/browserbase/__init__.py | 3 +- src/browserbase/_base_client.py | 22 ++++++++++ .../api_resources/sessions/test_downloads.py | 4 +- tests/api_resources/sessions/test_logs.py | 4 +- .../api_resources/sessions/test_recording.py | 4 +- tests/api_resources/sessions/test_uploads.py | 4 +- tests/api_resources/test_contexts.py | 4 +- tests/api_resources/test_extensions.py | 4 +- tests/api_resources/test_projects.py | 4 +- tests/api_resources/test_sessions.py | 4 +- tests/conftest.py | 43 ++++++++++++++++--- 15 files changed, 175 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 2d1471df..2f964be2 100644 --- a/README.md +++ b/README.md @@ -68,6 +68,40 @@ 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 PyPI +pip install browserbase[aiohttp] +``` + +Then you can enable it by instantiating the client with `http_client=DefaultAioHttpClient()`: + +```python +import os +import asyncio +from browserbase import DefaultAioHttpClient +from browserbase import AsyncBrowserbase + + +async def main() -> None: + async with AsyncBrowserbase( + api_key=os.environ.get("BROWSERBASE_API_KEY"), # This is the default and can be omitted + http_client=DefaultAioHttpClient(), + ) as client: + session = await client.sessions.create( + project_id="your_project_id", + ) + print(session.id) + + +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: diff --git a/pyproject.toml b/pyproject.toml index c8d76a5b..b0bd0a87 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,6 +37,8 @@ classifiers = [ Homepage = "https://github.com/browserbase/sdk-python" Repository = "https://github.com/browserbase/sdk-python" +[project.optional-dependencies] +aiohttp = ["aiohttp", "httpx_aiohttp>=0.1.6"] [tool.rye] managed = true diff --git a/requirements-dev.lock b/requirements-dev.lock index cc5b61a2..97a18b3f 100644 --- a/requirements-dev.lock +++ b/requirements-dev.lock @@ -10,6 +10,13 @@ # universal: false -e file:. +aiohappyeyeballs==2.6.1 + # via aiohttp +aiohttp==3.12.8 + # via browserbase + # via httpx-aiohttp +aiosignal==1.3.2 + # via aiohttp annotated-types==0.6.0 # via pydantic anyio==4.4.0 @@ -17,6 +24,10 @@ anyio==4.4.0 # via httpx argcomplete==3.1.2 # via nox +async-timeout==5.0.1 + # via aiohttp +attrs==25.3.0 + # via aiohttp certifi==2023.7.22 # via httpcore # via httpx @@ -34,16 +45,23 @@ execnet==2.1.1 # via pytest-xdist filelock==3.12.4 # via virtualenv +frozenlist==1.6.2 + # via aiohttp + # via aiosignal h11==0.14.0 # via httpcore httpcore==1.0.2 # via httpx httpx==0.28.1 # via browserbase + # via httpx-aiohttp # via respx +httpx-aiohttp==0.1.6 + # via browserbase idna==3.4 # via anyio # via httpx + # via yarl importlib-metadata==7.0.0 iniconfig==2.0.0 # via pytest @@ -51,6 +69,9 @@ markdown-it-py==3.0.0 # via rich mdurl==0.1.2 # via markdown-it-py +multidict==6.4.4 + # via aiohttp + # via yarl mypy==1.14.1 mypy-extensions==1.0.0 # via mypy @@ -65,6 +86,9 @@ platformdirs==3.11.0 # via virtualenv pluggy==1.5.0 # via pytest +propcache==0.3.1 + # via aiohttp + # via yarl pydantic==2.10.3 # via browserbase pydantic-core==2.27.1 @@ -98,11 +122,14 @@ tomli==2.0.2 typing-extensions==4.12.2 # via anyio # via browserbase + # via multidict # via mypy # via pydantic # via pydantic-core # via pyright virtualenv==20.24.5 # via nox +yarl==1.20.0 + # via aiohttp zipp==3.17.0 # via importlib-metadata diff --git a/requirements.lock b/requirements.lock index 9efa54d7..5a5c248f 100644 --- a/requirements.lock +++ b/requirements.lock @@ -10,11 +10,22 @@ # universal: false -e file:. +aiohappyeyeballs==2.6.1 + # via aiohttp +aiohttp==3.12.8 + # via browserbase + # via httpx-aiohttp +aiosignal==1.3.2 + # via aiohttp annotated-types==0.6.0 # via pydantic anyio==4.4.0 # via browserbase # via httpx +async-timeout==5.0.1 + # via aiohttp +attrs==25.3.0 + # via aiohttp certifi==2023.7.22 # via httpcore # via httpx @@ -22,15 +33,28 @@ distro==1.8.0 # via browserbase exceptiongroup==1.2.2 # via anyio +frozenlist==1.6.2 + # via aiohttp + # via aiosignal h11==0.14.0 # via httpcore httpcore==1.0.2 # via httpx httpx==0.28.1 # via browserbase + # via httpx-aiohttp +httpx-aiohttp==0.1.6 + # via browserbase idna==3.4 # via anyio # via httpx + # via yarl +multidict==6.4.4 + # via aiohttp + # via yarl +propcache==0.3.1 + # via aiohttp + # via yarl pydantic==2.10.3 # via browserbase pydantic-core==2.27.1 @@ -41,5 +65,8 @@ sniffio==1.3.0 typing-extensions==4.12.2 # via anyio # via browserbase + # via multidict # via pydantic # via pydantic-core +yarl==1.20.0 + # via aiohttp diff --git a/src/browserbase/__init__.py b/src/browserbase/__init__.py index e954b0ee..8e128845 100644 --- a/src/browserbase/__init__.py +++ b/src/browserbase/__init__.py @@ -36,7 +36,7 @@ UnprocessableEntityError, APIResponseValidationError, ) -from ._base_client import DefaultHttpxClient, DefaultAsyncHttpxClient +from ._base_client import DefaultHttpxClient, DefaultAioHttpClient, DefaultAsyncHttpxClient from ._utils._logs import setup_logging as _setup_logging __all__ = [ @@ -78,6 +78,7 @@ "DEFAULT_CONNECTION_LIMITS", "DefaultHttpxClient", "DefaultAsyncHttpxClient", + "DefaultAioHttpClient", ] if not _t.TYPE_CHECKING: diff --git a/src/browserbase/_base_client.py b/src/browserbase/_base_client.py index 9b8dc52a..e191446e 100644 --- a/src/browserbase/_base_client.py +++ b/src/browserbase/_base_client.py @@ -1289,6 +1289,24 @@ def __init__(self, **kwargs: Any) -> None: 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 @@ -1297,8 +1315,12 @@ def __init__(self, **kwargs: Any) -> None: 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): diff --git a/tests/api_resources/sessions/test_downloads.py b/tests/api_resources/sessions/test_downloads.py index 825ff786..10e84fdb 100644 --- a/tests/api_resources/sessions/test_downloads.py +++ b/tests/api_resources/sessions/test_downloads.py @@ -75,7 +75,9 @@ def test_path_params_list(self, client: Browserbase) -> None: class TestAsyncDownloads: - parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"]) + parametrize = pytest.mark.parametrize( + "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] + ) @parametrize @pytest.mark.respx(base_url=base_url) diff --git a/tests/api_resources/sessions/test_logs.py b/tests/api_resources/sessions/test_logs.py index c72002b3..eadde723 100644 --- a/tests/api_resources/sessions/test_logs.py +++ b/tests/api_resources/sessions/test_logs.py @@ -57,7 +57,9 @@ def test_path_params_list(self, client: Browserbase) -> None: class TestAsyncLogs: - parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"]) + parametrize = pytest.mark.parametrize( + "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] + ) @parametrize async def test_method_list(self, async_client: AsyncBrowserbase) -> None: diff --git a/tests/api_resources/sessions/test_recording.py b/tests/api_resources/sessions/test_recording.py index 0d7a542e..f1e97d07 100644 --- a/tests/api_resources/sessions/test_recording.py +++ b/tests/api_resources/sessions/test_recording.py @@ -57,7 +57,9 @@ def test_path_params_retrieve(self, client: Browserbase) -> None: class TestAsyncRecording: - parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"]) + parametrize = pytest.mark.parametrize( + "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] + ) @parametrize async def test_method_retrieve(self, async_client: AsyncBrowserbase) -> None: diff --git a/tests/api_resources/sessions/test_uploads.py b/tests/api_resources/sessions/test_uploads.py index f193256c..748b92e7 100644 --- a/tests/api_resources/sessions/test_uploads.py +++ b/tests/api_resources/sessions/test_uploads.py @@ -61,7 +61,9 @@ def test_path_params_create(self, client: Browserbase) -> None: class TestAsyncUploads: - parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"]) + parametrize = pytest.mark.parametrize( + "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] + ) @parametrize async def test_method_create(self, async_client: AsyncBrowserbase) -> None: diff --git a/tests/api_resources/test_contexts.py b/tests/api_resources/test_contexts.py index e53b7e11..72a1cf77 100644 --- a/tests/api_resources/test_contexts.py +++ b/tests/api_resources/test_contexts.py @@ -126,7 +126,9 @@ def test_path_params_update(self, client: Browserbase) -> None: class TestAsyncContexts: - parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"]) + parametrize = pytest.mark.parametrize( + "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] + ) @parametrize async def test_method_create(self, async_client: AsyncBrowserbase) -> None: diff --git a/tests/api_resources/test_extensions.py b/tests/api_resources/test_extensions.py index b7fec7a5..6b6a0183 100644 --- a/tests/api_resources/test_extensions.py +++ b/tests/api_resources/test_extensions.py @@ -126,7 +126,9 @@ def test_path_params_delete(self, client: Browserbase) -> None: class TestAsyncExtensions: - parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"]) + parametrize = pytest.mark.parametrize( + "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] + ) @parametrize async def test_method_create(self, async_client: AsyncBrowserbase) -> None: diff --git a/tests/api_resources/test_projects.py b/tests/api_resources/test_projects.py index 9e70d034..c8241bf8 100644 --- a/tests/api_resources/test_projects.py +++ b/tests/api_resources/test_projects.py @@ -120,7 +120,9 @@ def test_path_params_usage(self, client: Browserbase) -> None: class TestAsyncProjects: - parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"]) + parametrize = pytest.mark.parametrize( + "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] + ) @parametrize async def test_method_retrieve(self, async_client: AsyncBrowserbase) -> None: diff --git a/tests/api_resources/test_sessions.py b/tests/api_resources/test_sessions.py index 4a17c4f6..3c27348f 100644 --- a/tests/api_resources/test_sessions.py +++ b/tests/api_resources/test_sessions.py @@ -255,7 +255,9 @@ def test_path_params_debug(self, client: Browserbase) -> None: class TestAsyncSessions: - parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"]) + parametrize = pytest.mark.parametrize( + "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] + ) @parametrize async def test_method_create(self, async_client: AsyncBrowserbase) -> None: diff --git a/tests/conftest.py b/tests/conftest.py index b81e3b2d..7fc31c49 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -6,10 +6,12 @@ import logging from typing import TYPE_CHECKING, Iterator, AsyncIterator +import httpx import pytest from pytest_asyncio import is_async_test -from browserbase import Browserbase, AsyncBrowserbase +from browserbase import Browserbase, AsyncBrowserbase, DefaultAioHttpClient +from browserbase._utils import is_dict if TYPE_CHECKING: from _pytest.fixtures import FixtureRequest # pyright: ignore[reportPrivateImportUsage] @@ -27,6 +29,19 @@ def pytest_collection_modifyitems(items: list[pytest.Function]) -> None: 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") @@ -45,9 +60,25 @@ def client(request: FixtureRequest) -> Iterator[Browserbase]: @pytest.fixture(scope="session") async def async_client(request: FixtureRequest) -> AsyncIterator[AsyncBrowserbase]: - strict = getattr(request, "param", True) - if not isinstance(strict, bool): - raise TypeError(f"Unexpected fixture parameter type {type(strict)}, expected {bool}") - - async with AsyncBrowserbase(base_url=base_url, api_key=api_key, _strict_response_validation=strict) as client: + 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 AsyncBrowserbase( + base_url=base_url, api_key=api_key, _strict_response_validation=strict, http_client=http_client + ) as client: yield client From 8e14bbd62a81adec964830e2da467cb35e07ab36 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 24 Jun 2025 04:35:56 +0000 Subject: [PATCH 152/330] chore(tests): skip some failing tests on the latest python versions --- tests/test_client.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/test_client.py b/tests/test_client.py index 4c9938b1..e94506ec 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -191,6 +191,7 @@ def test_copy_signature(self) -> None: 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) -> None: options = FinalRequestOptions(method="get", url="/foo") @@ -1005,6 +1006,7 @@ def test_copy_signature(self) -> None: 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) -> None: options = FinalRequestOptions(method="get", url="/foo") From 6db99ef0fec456401ac2690da7527b88f25e2ec1 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 27 Jun 2025 02:38:54 +0000 Subject: [PATCH 153/330] =?UTF-8?q?fix(ci):=20release-doctor=20=E2=80=94?= =?UTF-8?q?=20report=20correct=20token=20name?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bin/check-release-environment | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/check-release-environment b/bin/check-release-environment index 6ad04d35..b845b0f4 100644 --- a/bin/check-release-environment +++ b/bin/check-release-environment @@ -3,7 +3,7 @@ errors=() if [ -z "${PYPI_TOKEN}" ]; then - errors+=("The BROWSERBASE_PYPI_TOKEN secret has not been set. Please set it in either this repository's secrets or your organization secrets.") + 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[@]} From 428a3dff6bb0390b50439b708f2ecf61b18400d0 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 28 Jun 2025 08:50:14 +0000 Subject: [PATCH 154/330] chore(ci): only run for pushes and fork pull requests --- .github/workflows/ci.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4946ac10..2b089258 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,6 +17,7 @@ jobs: timeout-minutes: 10 name: lint runs-on: ${{ github.repository == 'stainless-sdks/browserbase-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} + if: github.event_name == 'push' || github.event.pull_request.head.repo.fork steps: - uses: actions/checkout@v4 @@ -42,6 +43,7 @@ jobs: contents: read id-token: write runs-on: depot-ubuntu-24.04 + if: github.event_name == 'push' || github.event.pull_request.head.repo.fork steps: - uses: actions/checkout@v4 @@ -62,6 +64,7 @@ jobs: timeout-minutes: 10 name: test runs-on: ${{ github.repository == 'stainless-sdks/browserbase-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} + if: github.event_name == 'push' || github.event.pull_request.head.repo.fork steps: - uses: actions/checkout@v4 From 4398435d20e6dc813e068589900d004d2e1cc1fd Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 30 Jun 2025 02:33:48 +0000 Subject: [PATCH 155/330] fix(ci): correct conditional --- .github/workflows/ci.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2b089258..7a4492ea 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -36,14 +36,13 @@ jobs: run: ./scripts/lint upload: - if: github.repository == 'stainless-sdks/browserbase-python' + if: github.repository == 'stainless-sdks/browserbase-python' && (github.event_name == 'push' || github.event.pull_request.head.repo.fork) timeout-minutes: 10 name: upload permissions: contents: read id-token: write runs-on: depot-ubuntu-24.04 - if: github.event_name == 'push' || github.event.pull_request.head.repo.fork steps: - uses: actions/checkout@v4 From ff6e361a6f60584cb29e18f57cd7a661bba5378a Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 30 Jun 2025 09:16:16 +0000 Subject: [PATCH 156/330] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index 8cc23115..5b4609f9 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 18 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fbrowserbase-e2ed1b5267eeff92982918505349017b9155da2c7ab948787ab11cf9068af1b8.yml -openapi_spec_hash: 6639c21dccb52ca610cae833227a9791 +openapi_spec_hash: 77cba4a3c422b7378ecc7d57d84ff0b1 config_hash: b3ca4ec5b02e5333af51ebc2e9fdef1b From f434c2a4120067bffb5c0a883e191e103df61ef8 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 2 Jul 2025 05:32:44 +0000 Subject: [PATCH 157/330] chore(ci): change upload type --- .github/workflows/ci.yml | 18 ++++++++++++++++-- scripts/utils/upload-artifact.sh | 12 +++++++----- 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7a4492ea..455b6dc7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -35,10 +35,10 @@ jobs: - name: Run lints run: ./scripts/lint - upload: + build: if: github.repository == 'stainless-sdks/browserbase-python' && (github.event_name == 'push' || github.event.pull_request.head.repo.fork) timeout-minutes: 10 - name: upload + name: build permissions: contents: read id-token: write @@ -46,6 +46,20 @@ 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 dependencies + run: rye sync --all-features + + - name: Run build + run: rye build + - name: Get GitHub OIDC Token id: github-oidc uses: actions/github-script@v6 diff --git a/scripts/utils/upload-artifact.sh b/scripts/utils/upload-artifact.sh index 7c3d028a..4fa57664 100755 --- a/scripts/utils/upload-artifact.sh +++ b/scripts/utils/upload-artifact.sh @@ -1,7 +1,9 @@ #!/usr/bin/env bash set -exuo pipefail -RESPONSE=$(curl -X POST "$URL" \ +FILENAME=$(basename dist/*.whl) + +RESPONSE=$(curl -X POST "$URL?filename=$FILENAME" \ -H "Authorization: Bearer $AUTH" \ -H "Content-Type: application/json") @@ -12,13 +14,13 @@ if [[ "$SIGNED_URL" == "null" ]]; then exit 1 fi -UPLOAD_RESPONSE=$(tar -cz . | curl -v -X PUT \ - -H "Content-Type: application/gzip" \ - --data-binary @- "$SIGNED_URL" 2>&1) +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/browserbase-python/$SHA'\033[0m" + echo -e "\033[32mInstallation: pip install 'https://pkg.stainless.com/s/browserbase-python/$SHA/$FILENAME'\033[0m" else echo -e "\033[31mFailed to upload artifact.\033[0m" exit 1 From 357ba9c58a11c1e0329d7a6ca9763b55184fc15a Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 2 Jul 2025 08:36:37 +0000 Subject: [PATCH 158/330] feat(api): api update --- .github/workflows/ci.yml | 18 +-- .stats.yml | 4 +- README.md | 2 +- api.md | 28 ++-- scripts/utils/upload-artifact.sh | 12 +- src/browserbase/resources/contexts.py | 18 +-- src/browserbase/resources/extensions.py | 27 ++-- src/browserbase/resources/projects.py | 32 ++--- .../resources/sessions/sessions.py | 32 ++--- src/browserbase/types/__init__.py | 13 +- ...ontext.py => context_retrieve_response.py} | 4 +- ...ension.py => extension_create_response.py} | 4 +- .../types/extension_retrieve_response.py | 22 +++ .../types/project_list_response.py | 27 +++- ...roject.py => project_retrieve_response.py} | 4 +- ...ect_usage.py => project_usage_response.py} | 4 +- .../types/session_create_params.py | 46 +++--- ...live_urls.py => session_debug_response.py} | 4 +- .../types/session_list_response.py | 58 +++++++- ...{session.py => session_update_response.py} | 4 +- src/browserbase/types/sessions/__init__.py | 2 - .../types/sessions/log_list_response.py | 48 ++++++- .../sessions/recording_retrieve_response.py | 26 +++- src/browserbase/types/sessions/session_log.py | 46 ------ .../types/sessions/session_recording.py | 24 ---- .../api_resources/sessions/test_downloads.py | 36 +++-- tests/api_resources/sessions/test_logs.py | 12 +- .../api_resources/sessions/test_recording.py | 12 +- tests/api_resources/sessions/test_uploads.py | 12 +- tests/api_resources/test_contexts.py | 54 +++---- tests/api_resources/test_extensions.py | 50 +++---- tests/api_resources/test_projects.py | 50 +++---- tests/api_resources/test_sessions.py | 132 ++++++++++-------- tests/test_client.py | 28 ++-- 34 files changed, 495 insertions(+), 400 deletions(-) rename src/browserbase/types/{context.py => context_retrieve_response.py} (84%) rename src/browserbase/types/{extension.py => extension_create_response.py} (85%) create mode 100644 src/browserbase/types/extension_retrieve_response.py rename src/browserbase/types/{project.py => project_retrieve_response.py} (87%) rename src/browserbase/types/{project_usage.py => project_usage_response.py} (78%) rename src/browserbase/types/{session_live_urls.py => session_debug_response.py} (88%) rename src/browserbase/types/{session.py => session_update_response.py} (95%) delete mode 100644 src/browserbase/types/sessions/session_log.py delete mode 100644 src/browserbase/types/sessions/session_recording.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 455b6dc7..7a4492ea 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -35,10 +35,10 @@ jobs: - name: Run lints run: ./scripts/lint - build: + upload: if: github.repository == 'stainless-sdks/browserbase-python' && (github.event_name == 'push' || github.event.pull_request.head.repo.fork) timeout-minutes: 10 - name: build + name: upload permissions: contents: read id-token: write @@ -46,20 +46,6 @@ 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 dependencies - run: rye sync --all-features - - - name: Run build - run: rye build - - name: Get GitHub OIDC Token id: github-oidc uses: actions/github-script@v6 diff --git a/.stats.yml b/.stats.yml index 5b4609f9..fd2e8eac 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 18 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fbrowserbase-e2ed1b5267eeff92982918505349017b9155da2c7ab948787ab11cf9068af1b8.yml -openapi_spec_hash: 77cba4a3c422b7378ecc7d57d84ff0b1 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fbrowserbase-fe7af3b907d79ac271560c1d2e887ed741cfcc08cb8b75596094411a2091e223.yml +openapi_spec_hash: 999fb6ba05cd9be138ff94b787957ce9 config_hash: b3ca4ec5b02e5333af51ebc2e9fdef1b diff --git a/README.md b/README.md index 2f964be2..4e4ed1d6 100644 --- a/README.md +++ b/README.md @@ -121,7 +121,7 @@ from browserbase import Browserbase client = Browserbase() session = client.sessions.create( - project_id="projectId", + project_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", browser_settings={}, ) print(session.browser_settings) diff --git a/api.md b/api.md index dbb776f6..01454851 100644 --- a/api.md +++ b/api.md @@ -3,13 +3,13 @@ Types: ```python -from browserbase.types import Context, ContextCreateResponse, ContextUpdateResponse +from browserbase.types import ContextCreateResponse, ContextRetrieveResponse, ContextUpdateResponse ``` Methods: - client.contexts.create(\*\*params) -> ContextCreateResponse -- client.contexts.retrieve(id) -> Context +- client.contexts.retrieve(id) -> ContextRetrieveResponse - client.contexts.update(id) -> ContextUpdateResponse # Extensions @@ -17,13 +17,13 @@ Methods: Types: ```python -from browserbase.types import Extension +from browserbase.types import ExtensionCreateResponse, ExtensionRetrieveResponse ``` Methods: -- client.extensions.create(\*\*params) -> Extension -- client.extensions.retrieve(id) -> Extension +- client.extensions.create(\*\*params) -> ExtensionCreateResponse +- client.extensions.retrieve(id) -> ExtensionRetrieveResponse - client.extensions.delete(id) -> None # Projects @@ -31,14 +31,14 @@ Methods: Types: ```python -from browserbase.types import Project, ProjectUsage, ProjectListResponse +from browserbase.types import ProjectRetrieveResponse, ProjectListResponse, ProjectUsageResponse ``` Methods: -- client.projects.retrieve(id) -> Project +- client.projects.retrieve(id) -> ProjectRetrieveResponse - client.projects.list() -> ProjectListResponse -- client.projects.usage(id) -> ProjectUsage +- client.projects.usage(id) -> ProjectUsageResponse # Sessions @@ -46,11 +46,11 @@ Types: ```python from browserbase.types import ( - Session, - SessionLiveURLs, SessionCreateResponse, SessionRetrieveResponse, + SessionUpdateResponse, SessionListResponse, + SessionDebugResponse, ) ``` @@ -58,9 +58,9 @@ Methods: - client.sessions.create(\*\*params) -> SessionCreateResponse - client.sessions.retrieve(id) -> SessionRetrieveResponse -- client.sessions.update(id, \*\*params) -> Session +- client.sessions.update(id, \*\*params) -> SessionUpdateResponse - client.sessions.list(\*\*params) -> SessionListResponse -- client.sessions.debug(id) -> SessionLiveURLs +- client.sessions.debug(id) -> SessionDebugResponse ## Downloads @@ -73,7 +73,7 @@ Methods: Types: ```python -from browserbase.types.sessions import SessionLog, LogListResponse +from browserbase.types.sessions import LogListResponse ``` Methods: @@ -85,7 +85,7 @@ Methods: Types: ```python -from browserbase.types.sessions import SessionRecording, RecordingRetrieveResponse +from browserbase.types.sessions import RecordingRetrieveResponse ``` Methods: diff --git a/scripts/utils/upload-artifact.sh b/scripts/utils/upload-artifact.sh index 4fa57664..7c3d028a 100755 --- a/scripts/utils/upload-artifact.sh +++ b/scripts/utils/upload-artifact.sh @@ -1,9 +1,7 @@ #!/usr/bin/env bash set -exuo pipefail -FILENAME=$(basename dist/*.whl) - -RESPONSE=$(curl -X POST "$URL?filename=$FILENAME" \ +RESPONSE=$(curl -X POST "$URL" \ -H "Authorization: Bearer $AUTH" \ -H "Content-Type: application/json") @@ -14,13 +12,13 @@ if [[ "$SIGNED_URL" == "null" ]]; then exit 1 fi -UPLOAD_RESPONSE=$(curl -v -X PUT \ - -H "Content-Type: binary/octet-stream" \ - --data-binary "@dist/$FILENAME" "$SIGNED_URL" 2>&1) +UPLOAD_RESPONSE=$(tar -cz . | curl -v -X PUT \ + -H "Content-Type: application/gzip" \ + --data-binary @- "$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/browserbase-python/$SHA/$FILENAME'\033[0m" + echo -e "\033[32mInstallation: pip install 'https://pkg.stainless.com/s/browserbase-python/$SHA'\033[0m" else echo -e "\033[31mFailed to upload artifact.\033[0m" exit 1 diff --git a/src/browserbase/resources/contexts.py b/src/browserbase/resources/contexts.py index 0a438eda..bc4d1cc8 100644 --- a/src/browserbase/resources/contexts.py +++ b/src/browserbase/resources/contexts.py @@ -16,9 +16,9 @@ async_to_streamed_response_wrapper, ) from .._base_client import make_request_options -from ..types.context import Context from ..types.context_create_response import ContextCreateResponse from ..types.context_update_response import ContextUpdateResponse +from ..types.context_retrieve_response import ContextRetrieveResponse __all__ = ["ContextsResource", "AsyncContextsResource"] @@ -89,9 +89,9 @@ def retrieve( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> Context: + ) -> ContextRetrieveResponse: """ - Context + Get a Context Args: extra_headers: Send extra headers @@ -109,7 +109,7 @@ def retrieve( options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=Context, + cast_to=ContextRetrieveResponse, ) def update( @@ -124,7 +124,7 @@ def update( timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> ContextUpdateResponse: """ - Update Context + Update a Context Args: extra_headers: Send extra headers @@ -212,9 +212,9 @@ async def retrieve( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> Context: + ) -> ContextRetrieveResponse: """ - Context + Get a Context Args: extra_headers: Send extra headers @@ -232,7 +232,7 @@ async def retrieve( options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=Context, + cast_to=ContextRetrieveResponse, ) async def update( @@ -247,7 +247,7 @@ async def update( timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> ContextUpdateResponse: """ - Update Context + Update a Context Args: extra_headers: Send extra headers diff --git a/src/browserbase/resources/extensions.py b/src/browserbase/resources/extensions.py index c7b0fae7..4dcd248f 100644 --- a/src/browserbase/resources/extensions.py +++ b/src/browserbase/resources/extensions.py @@ -18,7 +18,8 @@ async_to_streamed_response_wrapper, ) from .._base_client import make_request_options -from ..types.extension import Extension +from ..types.extension_create_response import ExtensionCreateResponse +from ..types.extension_retrieve_response import ExtensionRetrieveResponse __all__ = ["ExtensionsResource", "AsyncExtensionsResource"] @@ -53,7 +54,7 @@ def create( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> Extension: + ) -> ExtensionCreateResponse: """ Upload an Extension @@ -79,7 +80,7 @@ def create( options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=Extension, + cast_to=ExtensionCreateResponse, ) def retrieve( @@ -92,9 +93,9 @@ def retrieve( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> Extension: + ) -> ExtensionRetrieveResponse: """ - Extension + Get an Extension Args: extra_headers: Send extra headers @@ -112,7 +113,7 @@ def retrieve( options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=Extension, + cast_to=ExtensionRetrieveResponse, ) def delete( @@ -127,7 +128,7 @@ def delete( timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> None: """ - Delete Extension + Delete an Extension Args: extra_headers: Send extra headers @@ -180,7 +181,7 @@ async def create( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> Extension: + ) -> ExtensionCreateResponse: """ Upload an Extension @@ -206,7 +207,7 @@ async def create( options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=Extension, + cast_to=ExtensionCreateResponse, ) async def retrieve( @@ -219,9 +220,9 @@ async def retrieve( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> Extension: + ) -> ExtensionRetrieveResponse: """ - Extension + Get an Extension Args: extra_headers: Send extra headers @@ -239,7 +240,7 @@ async def retrieve( options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=Extension, + cast_to=ExtensionRetrieveResponse, ) async def delete( @@ -254,7 +255,7 @@ async def delete( timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> None: """ - Delete Extension + Delete an Extension Args: extra_headers: Send extra headers diff --git a/src/browserbase/resources/projects.py b/src/browserbase/resources/projects.py index fb337a02..e0e73b40 100644 --- a/src/browserbase/resources/projects.py +++ b/src/browserbase/resources/projects.py @@ -14,9 +14,9 @@ async_to_streamed_response_wrapper, ) from .._base_client import make_request_options -from ..types.project import Project -from ..types.project_usage import ProjectUsage from ..types.project_list_response import ProjectListResponse +from ..types.project_usage_response import ProjectUsageResponse +from ..types.project_retrieve_response import ProjectRetrieveResponse __all__ = ["ProjectsResource", "AsyncProjectsResource"] @@ -51,9 +51,9 @@ def retrieve( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> Project: + ) -> ProjectRetrieveResponse: """ - Project + Get a Project Args: extra_headers: Send extra headers @@ -71,7 +71,7 @@ def retrieve( options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=Project, + cast_to=ProjectRetrieveResponse, ) def list( @@ -84,7 +84,7 @@ def list( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> ProjectListResponse: - """List projects""" + """List Projects""" return self._get( "/v1/projects", options=make_request_options( @@ -103,9 +103,9 @@ def usage( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> ProjectUsage: + ) -> ProjectUsageResponse: """ - Project Usage + Get Project Usage Args: extra_headers: Send extra headers @@ -123,7 +123,7 @@ def usage( options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=ProjectUsage, + cast_to=ProjectUsageResponse, ) @@ -157,9 +157,9 @@ async def retrieve( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> Project: + ) -> ProjectRetrieveResponse: """ - Project + Get a Project Args: extra_headers: Send extra headers @@ -177,7 +177,7 @@ async def retrieve( options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=Project, + cast_to=ProjectRetrieveResponse, ) async def list( @@ -190,7 +190,7 @@ async def list( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> ProjectListResponse: - """List projects""" + """List Projects""" return await self._get( "/v1/projects", options=make_request_options( @@ -209,9 +209,9 @@ async def usage( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> ProjectUsage: + ) -> ProjectUsageResponse: """ - Project Usage + Get Project Usage Args: extra_headers: Send extra headers @@ -229,7 +229,7 @@ async def usage( options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=ProjectUsage, + cast_to=ProjectUsageResponse, ) diff --git a/src/browserbase/resources/sessions/sessions.py b/src/browserbase/resources/sessions/sessions.py index bf3314ad..01a4943a 100644 --- a/src/browserbase/resources/sessions/sessions.py +++ b/src/browserbase/resources/sessions/sessions.py @@ -51,10 +51,10 @@ async_to_streamed_response_wrapper, ) from ..._base_client import make_request_options -from ...types.session import Session -from ...types.session_live_urls import SessionLiveURLs from ...types.session_list_response import SessionListResponse +from ...types.session_debug_response import SessionDebugResponse from ...types.session_create_response import SessionCreateResponse +from ...types.session_update_response import SessionUpdateResponse from ...types.session_retrieve_response import SessionRetrieveResponse __all__ = ["SessionsResource", "AsyncSessionsResource"] @@ -103,7 +103,7 @@ def create( browser_settings: session_create_params.BrowserSettings | NotGiven = NOT_GIVEN, extension_id: str | NotGiven = NOT_GIVEN, keep_alive: bool | NotGiven = NOT_GIVEN, - proxies: Union[bool, Iterable[session_create_params.ProxiesUnionMember1]] | NotGiven = NOT_GIVEN, + proxies: Union[Iterable[session_create_params.ProxiesUnionMember0], bool] | NotGiven = NOT_GIVEN, region: Literal["us-west-2", "us-east-1", "eu-central-1", "ap-southeast-1"] | NotGiven = NOT_GIVEN, api_timeout: int | NotGiven = NOT_GIVEN, user_metadata: Dict[str, object] | NotGiven = NOT_GIVEN, @@ -180,7 +180,7 @@ def retrieve( timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> SessionRetrieveResponse: """ - Session + Get a Session Args: extra_headers: Send extra headers @@ -213,8 +213,8 @@ def update( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> Session: - """Update Session + ) -> SessionUpdateResponse: + """Update a Session Args: project_id: The Project ID. @@ -247,7 +247,7 @@ def update( options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=Session, + cast_to=SessionUpdateResponse, ) def list( @@ -307,7 +307,7 @@ def debug( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> SessionLiveURLs: + ) -> SessionDebugResponse: """ Session Live URLs @@ -327,7 +327,7 @@ def debug( options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=SessionLiveURLs, + cast_to=SessionDebugResponse, ) @@ -374,7 +374,7 @@ async def create( browser_settings: session_create_params.BrowserSettings | NotGiven = NOT_GIVEN, extension_id: str | NotGiven = NOT_GIVEN, keep_alive: bool | NotGiven = NOT_GIVEN, - proxies: Union[bool, Iterable[session_create_params.ProxiesUnionMember1]] | NotGiven = NOT_GIVEN, + proxies: Union[Iterable[session_create_params.ProxiesUnionMember0], bool] | NotGiven = NOT_GIVEN, region: Literal["us-west-2", "us-east-1", "eu-central-1", "ap-southeast-1"] | NotGiven = NOT_GIVEN, api_timeout: int | NotGiven = NOT_GIVEN, user_metadata: Dict[str, object] | NotGiven = NOT_GIVEN, @@ -451,7 +451,7 @@ async def retrieve( timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> SessionRetrieveResponse: """ - Session + Get a Session Args: extra_headers: Send extra headers @@ -484,8 +484,8 @@ async def update( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> Session: - """Update Session + ) -> SessionUpdateResponse: + """Update a Session Args: project_id: The Project ID. @@ -518,7 +518,7 @@ async def update( options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=Session, + cast_to=SessionUpdateResponse, ) async def list( @@ -578,7 +578,7 @@ async def debug( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> SessionLiveURLs: + ) -> SessionDebugResponse: """ Session Live URLs @@ -598,7 +598,7 @@ async def debug( options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=SessionLiveURLs, + cast_to=SessionDebugResponse, ) diff --git a/src/browserbase/types/__init__.py b/src/browserbase/types/__init__.py index 4dd85ddb..20e2f905 100644 --- a/src/browserbase/types/__init__.py +++ b/src/browserbase/types/__init__.py @@ -2,20 +2,21 @@ from __future__ import annotations -from .context import Context as Context -from .project import Project as Project -from .session import Session as Session -from .extension import Extension as Extension -from .project_usage import ProjectUsage as ProjectUsage -from .session_live_urls import SessionLiveURLs as SessionLiveURLs from .session_list_params import SessionListParams as SessionListParams from .context_create_params import ContextCreateParams as ContextCreateParams from .project_list_response import ProjectListResponse as ProjectListResponse from .session_create_params import SessionCreateParams as SessionCreateParams from .session_list_response import SessionListResponse as SessionListResponse from .session_update_params import SessionUpdateParams as SessionUpdateParams +from .project_usage_response import ProjectUsageResponse as ProjectUsageResponse +from .session_debug_response import SessionDebugResponse as SessionDebugResponse from .context_create_response import ContextCreateResponse as ContextCreateResponse from .context_update_response import ContextUpdateResponse as ContextUpdateResponse from .extension_create_params import ExtensionCreateParams as ExtensionCreateParams from .session_create_response import SessionCreateResponse as SessionCreateResponse +from .session_update_response import SessionUpdateResponse as SessionUpdateResponse +from .context_retrieve_response import ContextRetrieveResponse as ContextRetrieveResponse +from .extension_create_response import ExtensionCreateResponse as ExtensionCreateResponse +from .project_retrieve_response import ProjectRetrieveResponse as ProjectRetrieveResponse from .session_retrieve_response import SessionRetrieveResponse as SessionRetrieveResponse +from .extension_retrieve_response import ExtensionRetrieveResponse as ExtensionRetrieveResponse diff --git a/src/browserbase/types/context.py b/src/browserbase/types/context_retrieve_response.py similarity index 84% rename from src/browserbase/types/context.py rename to src/browserbase/types/context_retrieve_response.py index cb5c32fd..c2cd6925 100644 --- a/src/browserbase/types/context.py +++ b/src/browserbase/types/context_retrieve_response.py @@ -6,10 +6,10 @@ from .._models import BaseModel -__all__ = ["Context"] +__all__ = ["ContextRetrieveResponse"] -class Context(BaseModel): +class ContextRetrieveResponse(BaseModel): id: str created_at: datetime = FieldInfo(alias="createdAt") diff --git a/src/browserbase/types/extension.py b/src/browserbase/types/extension_create_response.py similarity index 85% rename from src/browserbase/types/extension.py rename to src/browserbase/types/extension_create_response.py index 94582c34..d2b74f41 100644 --- a/src/browserbase/types/extension.py +++ b/src/browserbase/types/extension_create_response.py @@ -6,10 +6,10 @@ from .._models import BaseModel -__all__ = ["Extension"] +__all__ = ["ExtensionCreateResponse"] -class Extension(BaseModel): +class ExtensionCreateResponse(BaseModel): id: str created_at: datetime = FieldInfo(alias="createdAt") diff --git a/src/browserbase/types/extension_retrieve_response.py b/src/browserbase/types/extension_retrieve_response.py new file mode 100644 index 00000000..c786348e --- /dev/null +++ b/src/browserbase/types/extension_retrieve_response.py @@ -0,0 +1,22 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from datetime import datetime + +from pydantic import Field as FieldInfo + +from .._models import BaseModel + +__all__ = ["ExtensionRetrieveResponse"] + + +class ExtensionRetrieveResponse(BaseModel): + id: str + + created_at: datetime = FieldInfo(alias="createdAt") + + file_name: str = FieldInfo(alias="fileName") + + project_id: str = FieldInfo(alias="projectId") + """The Project ID linked to the uploaded Extension.""" + + updated_at: datetime = FieldInfo(alias="updatedAt") diff --git a/src/browserbase/types/project_list_response.py b/src/browserbase/types/project_list_response.py index 2d05a236..e364b520 100644 --- a/src/browserbase/types/project_list_response.py +++ b/src/browserbase/types/project_list_response.py @@ -1,10 +1,31 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. from typing import List +from datetime import datetime from typing_extensions import TypeAlias -from .project import Project +from pydantic import Field as FieldInfo -__all__ = ["ProjectListResponse"] +from .._models import BaseModel -ProjectListResponse: TypeAlias = List[Project] +__all__ = ["ProjectListResponse", "ProjectListResponseItem"] + + +class ProjectListResponseItem(BaseModel): + id: str + + concurrency: int + """The maximum number of sessions that this project can run concurrently.""" + + created_at: datetime = FieldInfo(alias="createdAt") + + default_timeout: int = FieldInfo(alias="defaultTimeout") + + name: str + + owner_id: str = FieldInfo(alias="ownerId") + + updated_at: datetime = FieldInfo(alias="updatedAt") + + +ProjectListResponse: TypeAlias = List[ProjectListResponseItem] diff --git a/src/browserbase/types/project.py b/src/browserbase/types/project_retrieve_response.py similarity index 87% rename from src/browserbase/types/project.py rename to src/browserbase/types/project_retrieve_response.py index dc3cf335..78126679 100644 --- a/src/browserbase/types/project.py +++ b/src/browserbase/types/project_retrieve_response.py @@ -6,10 +6,10 @@ from .._models import BaseModel -__all__ = ["Project"] +__all__ = ["ProjectRetrieveResponse"] -class Project(BaseModel): +class ProjectRetrieveResponse(BaseModel): id: str concurrency: int diff --git a/src/browserbase/types/project_usage.py b/src/browserbase/types/project_usage_response.py similarity index 78% rename from src/browserbase/types/project_usage.py rename to src/browserbase/types/project_usage_response.py index c8a03f5b..b52fccfe 100644 --- a/src/browserbase/types/project_usage.py +++ b/src/browserbase/types/project_usage_response.py @@ -4,10 +4,10 @@ from .._models import BaseModel -__all__ = ["ProjectUsage"] +__all__ = ["ProjectUsageResponse"] -class ProjectUsage(BaseModel): +class ProjectUsageResponse(BaseModel): browser_minutes: int = FieldInfo(alias="browserMinutes") proxy_bytes: int = FieldInfo(alias="proxyBytes") diff --git a/src/browserbase/types/session_create_params.py b/src/browserbase/types/session_create_params.py index 1f5324f8..a507f903 100644 --- a/src/browserbase/types/session_create_params.py +++ b/src/browserbase/types/session_create_params.py @@ -14,10 +14,10 @@ "BrowserSettingsFingerprint", "BrowserSettingsFingerprintScreen", "BrowserSettingsViewport", - "ProxiesUnionMember1", - "ProxiesUnionMember1BrowserbaseProxyConfig", - "ProxiesUnionMember1BrowserbaseProxyConfigGeolocation", - "ProxiesUnionMember1ExternalProxyConfig", + "ProxiesUnionMember0", + "ProxiesUnionMember0UnionMember0", + "ProxiesUnionMember0UnionMember0Geolocation", + "ProxiesUnionMember0UnionMember1", ] @@ -42,7 +42,7 @@ class SessionCreateParams(TypedDict, total=False): Available on the Hobby Plan and above. """ - proxies: Union[bool, Iterable[ProxiesUnionMember1]] + proxies: Union[Iterable[ProxiesUnionMember0], bool] """Proxy configuration. Can be true for default proxy, or an array of proxy configurations. @@ -74,13 +74,13 @@ class BrowserSettingsContext(TypedDict, total=False): class BrowserSettingsFingerprintScreen(TypedDict, total=False): - max_height: Annotated[int, PropertyInfo(alias="maxHeight")] + max_height: Required[Annotated[int, PropertyInfo(alias="maxHeight")]] - max_width: Annotated[int, PropertyInfo(alias="maxWidth")] + max_width: Required[Annotated[int, PropertyInfo(alias="maxWidth")]] - min_height: Annotated[int, PropertyInfo(alias="minHeight")] + min_height: Required[Annotated[int, PropertyInfo(alias="minHeight")]] - min_width: Annotated[int, PropertyInfo(alias="minWidth")] + min_width: Required[Annotated[int, PropertyInfo(alias="minWidth")]] class BrowserSettingsFingerprint(TypedDict, total=False): @@ -91,26 +91,20 @@ class BrowserSettingsFingerprint(TypedDict, total=False): http_version: Annotated[Literal["1", "2"], PropertyInfo(alias="httpVersion")] locales: List[str] - """ - Full list of locales is available - [here](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Language). - """ operating_systems: Annotated[ List[Literal["android", "ios", "linux", "macos", "windows"]], PropertyInfo(alias="operatingSystems") ] - """ - Note: `operatingSystems` set to `ios` or `android` requires `devices` to include - `"mobile"`. - """ screen: BrowserSettingsFingerprintScreen class BrowserSettingsViewport(TypedDict, total=False): height: int + """The height of the browser.""" width: int + """The width of the browser.""" class BrowserSettings(TypedDict, total=False): @@ -141,10 +135,6 @@ class BrowserSettings(TypedDict, total=False): """ fingerprint: BrowserSettingsFingerprint - """ - See usage examples - [in the Stealth Mode page](/features/stealth-mode#fingerprinting). - """ log_session: Annotated[bool, PropertyInfo(alias="logSession")] """Enable or disable session logging. Defaults to `true`.""" @@ -158,7 +148,7 @@ class BrowserSettings(TypedDict, total=False): viewport: BrowserSettingsViewport -class ProxiesUnionMember1BrowserbaseProxyConfigGeolocation(TypedDict, total=False): +class ProxiesUnionMember0UnionMember0Geolocation(TypedDict, total=False): country: Required[str] """Country code in ISO 3166-1 alpha-2 format""" @@ -169,7 +159,7 @@ class ProxiesUnionMember1BrowserbaseProxyConfigGeolocation(TypedDict, total=Fals """US state code (2 characters). Must also specify US as the country. Optional.""" -class ProxiesUnionMember1BrowserbaseProxyConfig(TypedDict, total=False): +class ProxiesUnionMember0UnionMember0(TypedDict, total=False): type: Required[Literal["browserbase"]] """Type of proxy. @@ -182,11 +172,11 @@ class ProxiesUnionMember1BrowserbaseProxyConfig(TypedDict, total=False): If omitted, defaults to all domains. Optional. """ - geolocation: ProxiesUnionMember1BrowserbaseProxyConfigGeolocation - """Configuration for geolocation""" + geolocation: ProxiesUnionMember0UnionMember0Geolocation + """Geographic location for the proxy. Optional.""" -class ProxiesUnionMember1ExternalProxyConfig(TypedDict, total=False): +class ProxiesUnionMember0UnionMember1(TypedDict, total=False): server: Required[str] """Server URL for external proxy. Required.""" @@ -206,6 +196,4 @@ class ProxiesUnionMember1ExternalProxyConfig(TypedDict, total=False): """Username for external proxy authentication. Optional.""" -ProxiesUnionMember1: TypeAlias = Union[ - ProxiesUnionMember1BrowserbaseProxyConfig, ProxiesUnionMember1ExternalProxyConfig -] +ProxiesUnionMember0: TypeAlias = Union[ProxiesUnionMember0UnionMember0, ProxiesUnionMember0UnionMember1] diff --git a/src/browserbase/types/session_live_urls.py b/src/browserbase/types/session_debug_response.py similarity index 88% rename from src/browserbase/types/session_live_urls.py rename to src/browserbase/types/session_debug_response.py index 3c7ba320..9cee7a77 100644 --- a/src/browserbase/types/session_live_urls.py +++ b/src/browserbase/types/session_debug_response.py @@ -6,7 +6,7 @@ from .._models import BaseModel -__all__ = ["SessionLiveURLs", "Page"] +__all__ = ["SessionDebugResponse", "Page"] class Page(BaseModel): @@ -23,7 +23,7 @@ class Page(BaseModel): url: str -class SessionLiveURLs(BaseModel): +class SessionDebugResponse(BaseModel): debugger_fullscreen_url: str = FieldInfo(alias="debuggerFullscreenUrl") debugger_url: str = FieldInfo(alias="debuggerUrl") diff --git a/src/browserbase/types/session_list_response.py b/src/browserbase/types/session_list_response.py index ca162ddb..4c1bd885 100644 --- a/src/browserbase/types/session_list_response.py +++ b/src/browserbase/types/session_list_response.py @@ -1,10 +1,58 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -from typing import List -from typing_extensions import TypeAlias +from typing import Dict, List, Optional +from datetime import datetime +from typing_extensions import Literal, TypeAlias -from .session import Session +from pydantic import Field as FieldInfo -__all__ = ["SessionListResponse"] +from .._models import BaseModel -SessionListResponse: TypeAlias = List[Session] +__all__ = ["SessionListResponse", "SessionListResponseItem"] + + +class SessionListResponseItem(BaseModel): + id: str + + created_at: datetime = FieldInfo(alias="createdAt") + + expires_at: datetime = FieldInfo(alias="expiresAt") + + keep_alive: bool = FieldInfo(alias="keepAlive") + """Indicates if the Session was created to be kept alive upon disconnections""" + + project_id: str = FieldInfo(alias="projectId") + """The Project ID linked to the Session.""" + + proxy_bytes: int = FieldInfo(alias="proxyBytes") + """Bytes used via the [Proxy](/features/stealth-mode#proxies-and-residential-ips)""" + + region: Literal["us-west-2", "us-east-1", "eu-central-1", "ap-southeast-1"] + """The region where the Session is running.""" + + started_at: datetime = FieldInfo(alias="startedAt") + + status: Literal["RUNNING", "ERROR", "TIMED_OUT", "COMPLETED"] + + updated_at: datetime = FieldInfo(alias="updatedAt") + + avg_cpu_usage: Optional[int] = FieldInfo(alias="avgCpuUsage", default=None) + """CPU used by the Session""" + + context_id: Optional[str] = FieldInfo(alias="contextId", default=None) + """Optional. The Context linked to the Session.""" + + ended_at: Optional[datetime] = FieldInfo(alias="endedAt", default=None) + + memory_usage: Optional[int] = FieldInfo(alias="memoryUsage", default=None) + """Memory used by the Session""" + + user_metadata: Optional[Dict[str, object]] = FieldInfo(alias="userMetadata", default=None) + """Arbitrary user metadata to attach to the session. + + To learn more about user metadata, see + [User Metadata](/features/sessions#user-metadata). + """ + + +SessionListResponse: TypeAlias = List[SessionListResponseItem] diff --git a/src/browserbase/types/session.py b/src/browserbase/types/session_update_response.py similarity index 95% rename from src/browserbase/types/session.py rename to src/browserbase/types/session_update_response.py index 16450e29..67a13711 100644 --- a/src/browserbase/types/session.py +++ b/src/browserbase/types/session_update_response.py @@ -8,10 +8,10 @@ from .._models import BaseModel -__all__ = ["Session"] +__all__ = ["SessionUpdateResponse"] -class Session(BaseModel): +class SessionUpdateResponse(BaseModel): id: str created_at: datetime = FieldInfo(alias="createdAt") diff --git a/src/browserbase/types/sessions/__init__.py b/src/browserbase/types/sessions/__init__.py index 0cef6b19..69d54703 100644 --- a/src/browserbase/types/sessions/__init__.py +++ b/src/browserbase/types/sessions/__init__.py @@ -2,9 +2,7 @@ from __future__ import annotations -from .session_log import SessionLog as SessionLog from .log_list_response import LogListResponse as LogListResponse -from .session_recording import SessionRecording as SessionRecording from .upload_create_params import UploadCreateParams as UploadCreateParams from .upload_create_response import UploadCreateResponse as UploadCreateResponse from .recording_retrieve_response import RecordingRetrieveResponse as RecordingRetrieveResponse diff --git a/src/browserbase/types/sessions/log_list_response.py b/src/browserbase/types/sessions/log_list_response.py index 2b325a8c..efd848ab 100644 --- a/src/browserbase/types/sessions/log_list_response.py +++ b/src/browserbase/types/sessions/log_list_response.py @@ -1,10 +1,50 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -from typing import List +from typing import Dict, List, Optional from typing_extensions import TypeAlias -from .session_log import SessionLog +from pydantic import Field as FieldInfo -__all__ = ["LogListResponse"] +from ..._models import BaseModel -LogListResponse: TypeAlias = List[SessionLog] +__all__ = ["LogListResponse", "LogListResponseItem", "LogListResponseItemRequest", "LogListResponseItemResponse"] + + +class LogListResponseItemRequest(BaseModel): + params: Dict[str, object] + + raw_body: str = FieldInfo(alias="rawBody") + + timestamp: Optional[int] = None + """milliseconds that have elapsed since the UNIX epoch""" + + +class LogListResponseItemResponse(BaseModel): + raw_body: str = FieldInfo(alias="rawBody") + + result: Dict[str, object] + + timestamp: Optional[int] = None + """milliseconds that have elapsed since the UNIX epoch""" + + +class LogListResponseItem(BaseModel): + method: str + + page_id: int = FieldInfo(alias="pageId") + + session_id: str = FieldInfo(alias="sessionId") + + frame_id: Optional[str] = FieldInfo(alias="frameId", default=None) + + loader_id: Optional[str] = FieldInfo(alias="loaderId", default=None) + + request: Optional[LogListResponseItemRequest] = None + + response: Optional[LogListResponseItemResponse] = None + + timestamp: Optional[int] = None + """milliseconds that have elapsed since the UNIX epoch""" + + +LogListResponse: TypeAlias = List[LogListResponseItem] diff --git a/src/browserbase/types/sessions/recording_retrieve_response.py b/src/browserbase/types/sessions/recording_retrieve_response.py index 951969bb..d3613b8c 100644 --- a/src/browserbase/types/sessions/recording_retrieve_response.py +++ b/src/browserbase/types/sessions/recording_retrieve_response.py @@ -1,10 +1,28 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -from typing import List +from typing import Dict, List from typing_extensions import TypeAlias -from .session_recording import SessionRecording +from pydantic import Field as FieldInfo -__all__ = ["RecordingRetrieveResponse"] +from ..._models import BaseModel -RecordingRetrieveResponse: TypeAlias = List[SessionRecording] +__all__ = ["RecordingRetrieveResponse", "RecordingRetrieveResponseItem"] + + +class RecordingRetrieveResponseItem(BaseModel): + data: Dict[str, object] + """ + See + [rrweb documentation](https://github.com/rrweb-io/rrweb/blob/master/docs/recipes/dive-into-event.md). + """ + + session_id: str = FieldInfo(alias="sessionId") + + timestamp: int + """milliseconds that have elapsed since the UNIX epoch""" + + type: int + + +RecordingRetrieveResponse: TypeAlias = List[RecordingRetrieveResponseItem] diff --git a/src/browserbase/types/sessions/session_log.py b/src/browserbase/types/sessions/session_log.py deleted file mode 100644 index 428f518a..00000000 --- a/src/browserbase/types/sessions/session_log.py +++ /dev/null @@ -1,46 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from typing import Dict, Optional - -from pydantic import Field as FieldInfo - -from ..._models import BaseModel - -__all__ = ["SessionLog", "Request", "Response"] - - -class Request(BaseModel): - params: Dict[str, object] - - raw_body: str = FieldInfo(alias="rawBody") - - timestamp: Optional[int] = None - """milliseconds that have elapsed since the UNIX epoch""" - - -class Response(BaseModel): - raw_body: str = FieldInfo(alias="rawBody") - - result: Dict[str, object] - - timestamp: Optional[int] = None - """milliseconds that have elapsed since the UNIX epoch""" - - -class SessionLog(BaseModel): - method: str - - page_id: int = FieldInfo(alias="pageId") - - session_id: str = FieldInfo(alias="sessionId") - - frame_id: Optional[str] = FieldInfo(alias="frameId", default=None) - - loader_id: Optional[str] = FieldInfo(alias="loaderId", default=None) - - request: Optional[Request] = None - - response: Optional[Response] = None - - timestamp: Optional[int] = None - """milliseconds that have elapsed since the UNIX epoch""" diff --git a/src/browserbase/types/sessions/session_recording.py b/src/browserbase/types/sessions/session_recording.py deleted file mode 100644 index c8471371..00000000 --- a/src/browserbase/types/sessions/session_recording.py +++ /dev/null @@ -1,24 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from typing import Dict - -from pydantic import Field as FieldInfo - -from ..._models import BaseModel - -__all__ = ["SessionRecording"] - - -class SessionRecording(BaseModel): - data: Dict[str, object] - """ - See - [rrweb documentation](https://github.com/rrweb-io/rrweb/blob/master/docs/recipes/dive-into-event.md). - """ - - session_id: str = FieldInfo(alias="sessionId") - - timestamp: int - """milliseconds that have elapsed since the UNIX epoch""" - - type: int diff --git a/tests/api_resources/sessions/test_downloads.py b/tests/api_resources/sessions/test_downloads.py index 10e84fdb..ed2feb9f 100644 --- a/tests/api_resources/sessions/test_downloads.py +++ b/tests/api_resources/sessions/test_downloads.py @@ -26,9 +26,11 @@ class TestDownloads: @parametrize @pytest.mark.respx(base_url=base_url) def test_method_list(self, client: Browserbase, respx_mock: MockRouter) -> None: - respx_mock.get("/v1/sessions/id/downloads").mock(return_value=httpx.Response(200, json={"foo": "bar"})) + respx_mock.get("/v1/sessions/182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e/downloads").mock( + return_value=httpx.Response(200, json={"foo": "bar"}) + ) download = client.sessions.downloads.list( - "id", + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", ) assert download.is_closed assert download.json() == {"foo": "bar"} @@ -38,10 +40,12 @@ def test_method_list(self, client: Browserbase, respx_mock: MockRouter) -> None: @parametrize @pytest.mark.respx(base_url=base_url) def test_raw_response_list(self, client: Browserbase, respx_mock: MockRouter) -> None: - respx_mock.get("/v1/sessions/id/downloads").mock(return_value=httpx.Response(200, json={"foo": "bar"})) + respx_mock.get("/v1/sessions/182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e/downloads").mock( + return_value=httpx.Response(200, json={"foo": "bar"}) + ) download = client.sessions.downloads.with_raw_response.list( - "id", + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", ) assert download.is_closed is True @@ -52,9 +56,11 @@ def test_raw_response_list(self, client: Browserbase, respx_mock: MockRouter) -> @parametrize @pytest.mark.respx(base_url=base_url) def test_streaming_response_list(self, client: Browserbase, respx_mock: MockRouter) -> None: - respx_mock.get("/v1/sessions/id/downloads").mock(return_value=httpx.Response(200, json={"foo": "bar"})) + respx_mock.get("/v1/sessions/182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e/downloads").mock( + return_value=httpx.Response(200, json={"foo": "bar"}) + ) with client.sessions.downloads.with_streaming_response.list( - "id", + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", ) as download: assert not download.is_closed assert download.http_request.headers.get("X-Stainless-Lang") == "python" @@ -82,9 +88,11 @@ class TestAsyncDownloads: @parametrize @pytest.mark.respx(base_url=base_url) async def test_method_list(self, async_client: AsyncBrowserbase, respx_mock: MockRouter) -> None: - respx_mock.get("/v1/sessions/id/downloads").mock(return_value=httpx.Response(200, json={"foo": "bar"})) + respx_mock.get("/v1/sessions/182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e/downloads").mock( + return_value=httpx.Response(200, json={"foo": "bar"}) + ) download = await async_client.sessions.downloads.list( - "id", + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", ) assert download.is_closed assert await download.json() == {"foo": "bar"} @@ -94,10 +102,12 @@ async def test_method_list(self, async_client: AsyncBrowserbase, respx_mock: Moc @parametrize @pytest.mark.respx(base_url=base_url) async def test_raw_response_list(self, async_client: AsyncBrowserbase, respx_mock: MockRouter) -> None: - respx_mock.get("/v1/sessions/id/downloads").mock(return_value=httpx.Response(200, json={"foo": "bar"})) + respx_mock.get("/v1/sessions/182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e/downloads").mock( + return_value=httpx.Response(200, json={"foo": "bar"}) + ) download = await async_client.sessions.downloads.with_raw_response.list( - "id", + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", ) assert download.is_closed is True @@ -108,9 +118,11 @@ async def test_raw_response_list(self, async_client: AsyncBrowserbase, respx_moc @parametrize @pytest.mark.respx(base_url=base_url) async def test_streaming_response_list(self, async_client: AsyncBrowserbase, respx_mock: MockRouter) -> None: - respx_mock.get("/v1/sessions/id/downloads").mock(return_value=httpx.Response(200, json={"foo": "bar"})) + respx_mock.get("/v1/sessions/182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e/downloads").mock( + return_value=httpx.Response(200, json={"foo": "bar"}) + ) async with async_client.sessions.downloads.with_streaming_response.list( - "id", + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", ) as download: assert not download.is_closed assert download.http_request.headers.get("X-Stainless-Lang") == "python" diff --git a/tests/api_resources/sessions/test_logs.py b/tests/api_resources/sessions/test_logs.py index eadde723..96d4779e 100644 --- a/tests/api_resources/sessions/test_logs.py +++ b/tests/api_resources/sessions/test_logs.py @@ -20,14 +20,14 @@ class TestLogs: @parametrize def test_method_list(self, client: Browserbase) -> None: log = client.sessions.logs.list( - "id", + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", ) assert_matches_type(LogListResponse, log, path=["response"]) @parametrize def test_raw_response_list(self, client: Browserbase) -> None: response = client.sessions.logs.with_raw_response.list( - "id", + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", ) assert response.is_closed is True @@ -38,7 +38,7 @@ def test_raw_response_list(self, client: Browserbase) -> None: @parametrize def test_streaming_response_list(self, client: Browserbase) -> None: with client.sessions.logs.with_streaming_response.list( - "id", + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -64,14 +64,14 @@ class TestAsyncLogs: @parametrize async def test_method_list(self, async_client: AsyncBrowserbase) -> None: log = await async_client.sessions.logs.list( - "id", + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", ) assert_matches_type(LogListResponse, log, path=["response"]) @parametrize async def test_raw_response_list(self, async_client: AsyncBrowserbase) -> None: response = await async_client.sessions.logs.with_raw_response.list( - "id", + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", ) assert response.is_closed is True @@ -82,7 +82,7 @@ async def test_raw_response_list(self, async_client: AsyncBrowserbase) -> None: @parametrize async def test_streaming_response_list(self, async_client: AsyncBrowserbase) -> None: async with async_client.sessions.logs.with_streaming_response.list( - "id", + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" diff --git a/tests/api_resources/sessions/test_recording.py b/tests/api_resources/sessions/test_recording.py index f1e97d07..c60c0c1b 100644 --- a/tests/api_resources/sessions/test_recording.py +++ b/tests/api_resources/sessions/test_recording.py @@ -20,14 +20,14 @@ class TestRecording: @parametrize def test_method_retrieve(self, client: Browserbase) -> None: recording = client.sessions.recording.retrieve( - "id", + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", ) assert_matches_type(RecordingRetrieveResponse, recording, path=["response"]) @parametrize def test_raw_response_retrieve(self, client: Browserbase) -> None: response = client.sessions.recording.with_raw_response.retrieve( - "id", + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", ) assert response.is_closed is True @@ -38,7 +38,7 @@ def test_raw_response_retrieve(self, client: Browserbase) -> None: @parametrize def test_streaming_response_retrieve(self, client: Browserbase) -> None: with client.sessions.recording.with_streaming_response.retrieve( - "id", + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -64,14 +64,14 @@ class TestAsyncRecording: @parametrize async def test_method_retrieve(self, async_client: AsyncBrowserbase) -> None: recording = await async_client.sessions.recording.retrieve( - "id", + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", ) assert_matches_type(RecordingRetrieveResponse, recording, path=["response"]) @parametrize async def test_raw_response_retrieve(self, async_client: AsyncBrowserbase) -> None: response = await async_client.sessions.recording.with_raw_response.retrieve( - "id", + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", ) assert response.is_closed is True @@ -82,7 +82,7 @@ async def test_raw_response_retrieve(self, async_client: AsyncBrowserbase) -> No @parametrize async def test_streaming_response_retrieve(self, async_client: AsyncBrowserbase) -> None: async with async_client.sessions.recording.with_streaming_response.retrieve( - "id", + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" diff --git a/tests/api_resources/sessions/test_uploads.py b/tests/api_resources/sessions/test_uploads.py index 748b92e7..0a8b0fae 100644 --- a/tests/api_resources/sessions/test_uploads.py +++ b/tests/api_resources/sessions/test_uploads.py @@ -20,7 +20,7 @@ class TestUploads: @parametrize def test_method_create(self, client: Browserbase) -> None: upload = client.sessions.uploads.create( - id="id", + id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", file=b"raw file contents", ) assert_matches_type(UploadCreateResponse, upload, path=["response"]) @@ -28,7 +28,7 @@ def test_method_create(self, client: Browserbase) -> None: @parametrize def test_raw_response_create(self, client: Browserbase) -> None: response = client.sessions.uploads.with_raw_response.create( - id="id", + id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", file=b"raw file contents", ) @@ -40,7 +40,7 @@ def test_raw_response_create(self, client: Browserbase) -> None: @parametrize def test_streaming_response_create(self, client: Browserbase) -> None: with client.sessions.uploads.with_streaming_response.create( - id="id", + id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", file=b"raw file contents", ) as response: assert not response.is_closed @@ -68,7 +68,7 @@ class TestAsyncUploads: @parametrize async def test_method_create(self, async_client: AsyncBrowserbase) -> None: upload = await async_client.sessions.uploads.create( - id="id", + id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", file=b"raw file contents", ) assert_matches_type(UploadCreateResponse, upload, path=["response"]) @@ -76,7 +76,7 @@ async def test_method_create(self, async_client: AsyncBrowserbase) -> None: @parametrize async def test_raw_response_create(self, async_client: AsyncBrowserbase) -> None: response = await async_client.sessions.uploads.with_raw_response.create( - id="id", + id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", file=b"raw file contents", ) @@ -88,7 +88,7 @@ async def test_raw_response_create(self, async_client: AsyncBrowserbase) -> None @parametrize async def test_streaming_response_create(self, async_client: AsyncBrowserbase) -> None: async with async_client.sessions.uploads.with_streaming_response.create( - id="id", + id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", file=b"raw file contents", ) as response: assert not response.is_closed diff --git a/tests/api_resources/test_contexts.py b/tests/api_resources/test_contexts.py index 72a1cf77..d977efb2 100644 --- a/tests/api_resources/test_contexts.py +++ b/tests/api_resources/test_contexts.py @@ -9,7 +9,11 @@ from browserbase import Browserbase, AsyncBrowserbase from tests.utils import assert_matches_type -from browserbase.types import Context, ContextCreateResponse, ContextUpdateResponse +from browserbase.types import ( + ContextCreateResponse, + ContextUpdateResponse, + ContextRetrieveResponse, +) base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") @@ -20,14 +24,14 @@ class TestContexts: @parametrize def test_method_create(self, client: Browserbase) -> None: context = client.contexts.create( - project_id="projectId", + project_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", ) assert_matches_type(ContextCreateResponse, context, path=["response"]) @parametrize def test_raw_response_create(self, client: Browserbase) -> None: response = client.contexts.with_raw_response.create( - project_id="projectId", + project_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", ) assert response.is_closed is True @@ -38,7 +42,7 @@ def test_raw_response_create(self, client: Browserbase) -> None: @parametrize def test_streaming_response_create(self, client: Browserbase) -> None: with client.contexts.with_streaming_response.create( - project_id="projectId", + project_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -51,31 +55,31 @@ def test_streaming_response_create(self, client: Browserbase) -> None: @parametrize def test_method_retrieve(self, client: Browserbase) -> None: context = client.contexts.retrieve( - "id", + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", ) - assert_matches_type(Context, context, path=["response"]) + assert_matches_type(ContextRetrieveResponse, context, path=["response"]) @parametrize def test_raw_response_retrieve(self, client: Browserbase) -> None: response = client.contexts.with_raw_response.retrieve( - "id", + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", ) assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" context = response.parse() - assert_matches_type(Context, context, path=["response"]) + assert_matches_type(ContextRetrieveResponse, context, path=["response"]) @parametrize def test_streaming_response_retrieve(self, client: Browserbase) -> None: with client.contexts.with_streaming_response.retrieve( - "id", + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" context = response.parse() - assert_matches_type(Context, context, path=["response"]) + assert_matches_type(ContextRetrieveResponse, context, path=["response"]) assert cast(Any, response.is_closed) is True @@ -89,14 +93,14 @@ def test_path_params_retrieve(self, client: Browserbase) -> None: @parametrize def test_method_update(self, client: Browserbase) -> None: context = client.contexts.update( - "id", + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", ) assert_matches_type(ContextUpdateResponse, context, path=["response"]) @parametrize def test_raw_response_update(self, client: Browserbase) -> None: response = client.contexts.with_raw_response.update( - "id", + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", ) assert response.is_closed is True @@ -107,7 +111,7 @@ def test_raw_response_update(self, client: Browserbase) -> None: @parametrize def test_streaming_response_update(self, client: Browserbase) -> None: with client.contexts.with_streaming_response.update( - "id", + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -133,14 +137,14 @@ class TestAsyncContexts: @parametrize async def test_method_create(self, async_client: AsyncBrowserbase) -> None: context = await async_client.contexts.create( - project_id="projectId", + project_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", ) assert_matches_type(ContextCreateResponse, context, path=["response"]) @parametrize async def test_raw_response_create(self, async_client: AsyncBrowserbase) -> None: response = await async_client.contexts.with_raw_response.create( - project_id="projectId", + project_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", ) assert response.is_closed is True @@ -151,7 +155,7 @@ async def test_raw_response_create(self, async_client: AsyncBrowserbase) -> None @parametrize async def test_streaming_response_create(self, async_client: AsyncBrowserbase) -> None: async with async_client.contexts.with_streaming_response.create( - project_id="projectId", + project_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -164,31 +168,31 @@ async def test_streaming_response_create(self, async_client: AsyncBrowserbase) - @parametrize async def test_method_retrieve(self, async_client: AsyncBrowserbase) -> None: context = await async_client.contexts.retrieve( - "id", + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", ) - assert_matches_type(Context, context, path=["response"]) + assert_matches_type(ContextRetrieveResponse, context, path=["response"]) @parametrize async def test_raw_response_retrieve(self, async_client: AsyncBrowserbase) -> None: response = await async_client.contexts.with_raw_response.retrieve( - "id", + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", ) assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" context = await response.parse() - assert_matches_type(Context, context, path=["response"]) + assert_matches_type(ContextRetrieveResponse, context, path=["response"]) @parametrize async def test_streaming_response_retrieve(self, async_client: AsyncBrowserbase) -> None: async with async_client.contexts.with_streaming_response.retrieve( - "id", + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" context = await response.parse() - assert_matches_type(Context, context, path=["response"]) + assert_matches_type(ContextRetrieveResponse, context, path=["response"]) assert cast(Any, response.is_closed) is True @@ -202,14 +206,14 @@ async def test_path_params_retrieve(self, async_client: AsyncBrowserbase) -> Non @parametrize async def test_method_update(self, async_client: AsyncBrowserbase) -> None: context = await async_client.contexts.update( - "id", + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", ) assert_matches_type(ContextUpdateResponse, context, path=["response"]) @parametrize async def test_raw_response_update(self, async_client: AsyncBrowserbase) -> None: response = await async_client.contexts.with_raw_response.update( - "id", + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", ) assert response.is_closed is True @@ -220,7 +224,7 @@ async def test_raw_response_update(self, async_client: AsyncBrowserbase) -> None @parametrize async def test_streaming_response_update(self, async_client: AsyncBrowserbase) -> None: async with async_client.contexts.with_streaming_response.update( - "id", + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" diff --git a/tests/api_resources/test_extensions.py b/tests/api_resources/test_extensions.py index 6b6a0183..2a6e0ce2 100644 --- a/tests/api_resources/test_extensions.py +++ b/tests/api_resources/test_extensions.py @@ -9,7 +9,7 @@ from browserbase import Browserbase, AsyncBrowserbase from tests.utils import assert_matches_type -from browserbase.types import Extension +from browserbase.types import ExtensionCreateResponse, ExtensionRetrieveResponse base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") @@ -22,7 +22,7 @@ def test_method_create(self, client: Browserbase) -> None: extension = client.extensions.create( file=b"raw file contents", ) - assert_matches_type(Extension, extension, path=["response"]) + assert_matches_type(ExtensionCreateResponse, extension, path=["response"]) @parametrize def test_raw_response_create(self, client: Browserbase) -> None: @@ -33,7 +33,7 @@ def test_raw_response_create(self, client: Browserbase) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" extension = response.parse() - assert_matches_type(Extension, extension, path=["response"]) + assert_matches_type(ExtensionCreateResponse, extension, path=["response"]) @parametrize def test_streaming_response_create(self, client: Browserbase) -> None: @@ -44,38 +44,38 @@ def test_streaming_response_create(self, client: Browserbase) -> None: assert response.http_request.headers.get("X-Stainless-Lang") == "python" extension = response.parse() - assert_matches_type(Extension, extension, path=["response"]) + assert_matches_type(ExtensionCreateResponse, extension, path=["response"]) assert cast(Any, response.is_closed) is True @parametrize def test_method_retrieve(self, client: Browserbase) -> None: extension = client.extensions.retrieve( - "id", + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", ) - assert_matches_type(Extension, extension, path=["response"]) + assert_matches_type(ExtensionRetrieveResponse, extension, path=["response"]) @parametrize def test_raw_response_retrieve(self, client: Browserbase) -> None: response = client.extensions.with_raw_response.retrieve( - "id", + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", ) assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" extension = response.parse() - assert_matches_type(Extension, extension, path=["response"]) + assert_matches_type(ExtensionRetrieveResponse, extension, path=["response"]) @parametrize def test_streaming_response_retrieve(self, client: Browserbase) -> None: with client.extensions.with_streaming_response.retrieve( - "id", + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" extension = response.parse() - assert_matches_type(Extension, extension, path=["response"]) + assert_matches_type(ExtensionRetrieveResponse, extension, path=["response"]) assert cast(Any, response.is_closed) is True @@ -89,14 +89,14 @@ def test_path_params_retrieve(self, client: Browserbase) -> None: @parametrize def test_method_delete(self, client: Browserbase) -> None: extension = client.extensions.delete( - "id", + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", ) assert extension is None @parametrize def test_raw_response_delete(self, client: Browserbase) -> None: response = client.extensions.with_raw_response.delete( - "id", + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", ) assert response.is_closed is True @@ -107,7 +107,7 @@ def test_raw_response_delete(self, client: Browserbase) -> None: @parametrize def test_streaming_response_delete(self, client: Browserbase) -> None: with client.extensions.with_streaming_response.delete( - "id", + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -135,7 +135,7 @@ async def test_method_create(self, async_client: AsyncBrowserbase) -> None: extension = await async_client.extensions.create( file=b"raw file contents", ) - assert_matches_type(Extension, extension, path=["response"]) + assert_matches_type(ExtensionCreateResponse, extension, path=["response"]) @parametrize async def test_raw_response_create(self, async_client: AsyncBrowserbase) -> None: @@ -146,7 +146,7 @@ async def test_raw_response_create(self, async_client: AsyncBrowserbase) -> None assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" extension = await response.parse() - assert_matches_type(Extension, extension, path=["response"]) + assert_matches_type(ExtensionCreateResponse, extension, path=["response"]) @parametrize async def test_streaming_response_create(self, async_client: AsyncBrowserbase) -> None: @@ -157,38 +157,38 @@ async def test_streaming_response_create(self, async_client: AsyncBrowserbase) - assert response.http_request.headers.get("X-Stainless-Lang") == "python" extension = await response.parse() - assert_matches_type(Extension, extension, path=["response"]) + assert_matches_type(ExtensionCreateResponse, extension, path=["response"]) assert cast(Any, response.is_closed) is True @parametrize async def test_method_retrieve(self, async_client: AsyncBrowserbase) -> None: extension = await async_client.extensions.retrieve( - "id", + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", ) - assert_matches_type(Extension, extension, path=["response"]) + assert_matches_type(ExtensionRetrieveResponse, extension, path=["response"]) @parametrize async def test_raw_response_retrieve(self, async_client: AsyncBrowserbase) -> None: response = await async_client.extensions.with_raw_response.retrieve( - "id", + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", ) assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" extension = await response.parse() - assert_matches_type(Extension, extension, path=["response"]) + assert_matches_type(ExtensionRetrieveResponse, extension, path=["response"]) @parametrize async def test_streaming_response_retrieve(self, async_client: AsyncBrowserbase) -> None: async with async_client.extensions.with_streaming_response.retrieve( - "id", + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" extension = await response.parse() - assert_matches_type(Extension, extension, path=["response"]) + assert_matches_type(ExtensionRetrieveResponse, extension, path=["response"]) assert cast(Any, response.is_closed) is True @@ -202,14 +202,14 @@ async def test_path_params_retrieve(self, async_client: AsyncBrowserbase) -> Non @parametrize async def test_method_delete(self, async_client: AsyncBrowserbase) -> None: extension = await async_client.extensions.delete( - "id", + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", ) assert extension is None @parametrize async def test_raw_response_delete(self, async_client: AsyncBrowserbase) -> None: response = await async_client.extensions.with_raw_response.delete( - "id", + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", ) assert response.is_closed is True @@ -220,7 +220,7 @@ async def test_raw_response_delete(self, async_client: AsyncBrowserbase) -> None @parametrize async def test_streaming_response_delete(self, async_client: AsyncBrowserbase) -> None: async with async_client.extensions.with_streaming_response.delete( - "id", + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" diff --git a/tests/api_resources/test_projects.py b/tests/api_resources/test_projects.py index c8241bf8..5217503f 100644 --- a/tests/api_resources/test_projects.py +++ b/tests/api_resources/test_projects.py @@ -9,7 +9,7 @@ from browserbase import Browserbase, AsyncBrowserbase from tests.utils import assert_matches_type -from browserbase.types import Project, ProjectUsage, ProjectListResponse +from browserbase.types import ProjectListResponse, ProjectUsageResponse, ProjectRetrieveResponse base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") @@ -20,31 +20,31 @@ class TestProjects: @parametrize def test_method_retrieve(self, client: Browserbase) -> None: project = client.projects.retrieve( - "id", + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", ) - assert_matches_type(Project, project, path=["response"]) + assert_matches_type(ProjectRetrieveResponse, project, path=["response"]) @parametrize def test_raw_response_retrieve(self, client: Browserbase) -> None: response = client.projects.with_raw_response.retrieve( - "id", + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", ) assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" project = response.parse() - assert_matches_type(Project, project, path=["response"]) + assert_matches_type(ProjectRetrieveResponse, project, path=["response"]) @parametrize def test_streaming_response_retrieve(self, client: Browserbase) -> None: with client.projects.with_streaming_response.retrieve( - "id", + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" project = response.parse() - assert_matches_type(Project, project, path=["response"]) + assert_matches_type(ProjectRetrieveResponse, project, path=["response"]) assert cast(Any, response.is_closed) is True @@ -83,31 +83,31 @@ def test_streaming_response_list(self, client: Browserbase) -> None: @parametrize def test_method_usage(self, client: Browserbase) -> None: project = client.projects.usage( - "id", + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", ) - assert_matches_type(ProjectUsage, project, path=["response"]) + assert_matches_type(ProjectUsageResponse, project, path=["response"]) @parametrize def test_raw_response_usage(self, client: Browserbase) -> None: response = client.projects.with_raw_response.usage( - "id", + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", ) assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" project = response.parse() - assert_matches_type(ProjectUsage, project, path=["response"]) + assert_matches_type(ProjectUsageResponse, project, path=["response"]) @parametrize def test_streaming_response_usage(self, client: Browserbase) -> None: with client.projects.with_streaming_response.usage( - "id", + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" project = response.parse() - assert_matches_type(ProjectUsage, project, path=["response"]) + assert_matches_type(ProjectUsageResponse, project, path=["response"]) assert cast(Any, response.is_closed) is True @@ -127,31 +127,31 @@ class TestAsyncProjects: @parametrize async def test_method_retrieve(self, async_client: AsyncBrowserbase) -> None: project = await async_client.projects.retrieve( - "id", + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", ) - assert_matches_type(Project, project, path=["response"]) + assert_matches_type(ProjectRetrieveResponse, project, path=["response"]) @parametrize async def test_raw_response_retrieve(self, async_client: AsyncBrowserbase) -> None: response = await async_client.projects.with_raw_response.retrieve( - "id", + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", ) assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" project = await response.parse() - assert_matches_type(Project, project, path=["response"]) + assert_matches_type(ProjectRetrieveResponse, project, path=["response"]) @parametrize async def test_streaming_response_retrieve(self, async_client: AsyncBrowserbase) -> None: async with async_client.projects.with_streaming_response.retrieve( - "id", + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" project = await response.parse() - assert_matches_type(Project, project, path=["response"]) + assert_matches_type(ProjectRetrieveResponse, project, path=["response"]) assert cast(Any, response.is_closed) is True @@ -190,31 +190,31 @@ async def test_streaming_response_list(self, async_client: AsyncBrowserbase) -> @parametrize async def test_method_usage(self, async_client: AsyncBrowserbase) -> None: project = await async_client.projects.usage( - "id", + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", ) - assert_matches_type(ProjectUsage, project, path=["response"]) + assert_matches_type(ProjectUsageResponse, project, path=["response"]) @parametrize async def test_raw_response_usage(self, async_client: AsyncBrowserbase) -> None: response = await async_client.projects.with_raw_response.usage( - "id", + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", ) assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" project = await response.parse() - assert_matches_type(ProjectUsage, project, path=["response"]) + assert_matches_type(ProjectUsageResponse, project, path=["response"]) @parametrize async def test_streaming_response_usage(self, async_client: AsyncBrowserbase) -> None: async with async_client.projects.with_streaming_response.usage( - "id", + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" project = await response.parse() - assert_matches_type(ProjectUsage, project, path=["response"]) + assert_matches_type(ProjectUsageResponse, project, path=["response"]) assert cast(Any, response.is_closed) is True diff --git a/tests/api_resources/test_sessions.py b/tests/api_resources/test_sessions.py index 3c27348f..11471ecd 100644 --- a/tests/api_resources/test_sessions.py +++ b/tests/api_resources/test_sessions.py @@ -10,10 +10,10 @@ from browserbase import Browserbase, AsyncBrowserbase from tests.utils import assert_matches_type from browserbase.types import ( - Session, - SessionLiveURLs, SessionListResponse, + SessionDebugResponse, SessionCreateResponse, + SessionUpdateResponse, SessionRetrieveResponse, ) @@ -26,24 +26,24 @@ class TestSessions: @parametrize def test_method_create(self, client: Browserbase) -> None: session = client.sessions.create( - project_id="projectId", + project_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", ) assert_matches_type(SessionCreateResponse, session, path=["response"]) @parametrize def test_method_create_with_all_params(self, client: Browserbase) -> None: session = client.sessions.create( - project_id="projectId", + project_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", browser_settings={ "advanced_stealth": True, "block_ads": True, "captcha_image_selector": "captchaImageSelector", "captcha_input_selector": "captchaInputSelector", "context": { - "id": "id", + "id": "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", "persist": True, }, - "extension_id": "extensionId", + "extension_id": "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", "fingerprint": { "browsers": ["chrome"], "devices": ["desktop"], @@ -65,9 +65,19 @@ def test_method_create_with_all_params(self, client: Browserbase) -> None: "width": 0, }, }, - extension_id="extensionId", + extension_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", keep_alive=True, - proxies=True, + proxies=[ + { + "type": "browserbase", + "domain_pattern": "domainPattern", + "geolocation": { + "country": "xx", + "city": "city", + "state": "xx", + }, + } + ], region="us-west-2", api_timeout=60, user_metadata={"foo": "bar"}, @@ -77,7 +87,7 @@ def test_method_create_with_all_params(self, client: Browserbase) -> None: @parametrize def test_raw_response_create(self, client: Browserbase) -> None: response = client.sessions.with_raw_response.create( - project_id="projectId", + project_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", ) assert response.is_closed is True @@ -88,7 +98,7 @@ def test_raw_response_create(self, client: Browserbase) -> None: @parametrize def test_streaming_response_create(self, client: Browserbase) -> None: with client.sessions.with_streaming_response.create( - project_id="projectId", + project_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -101,14 +111,14 @@ def test_streaming_response_create(self, client: Browserbase) -> None: @parametrize def test_method_retrieve(self, client: Browserbase) -> None: session = client.sessions.retrieve( - "id", + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", ) assert_matches_type(SessionRetrieveResponse, session, path=["response"]) @parametrize def test_raw_response_retrieve(self, client: Browserbase) -> None: response = client.sessions.with_raw_response.retrieve( - "id", + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", ) assert response.is_closed is True @@ -119,7 +129,7 @@ def test_raw_response_retrieve(self, client: Browserbase) -> None: @parametrize def test_streaming_response_retrieve(self, client: Browserbase) -> None: with client.sessions.with_streaming_response.retrieve( - "id", + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -139,37 +149,37 @@ def test_path_params_retrieve(self, client: Browserbase) -> None: @parametrize def test_method_update(self, client: Browserbase) -> None: session = client.sessions.update( - id="id", - project_id="projectId", + id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + project_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", status="REQUEST_RELEASE", ) - assert_matches_type(Session, session, path=["response"]) + assert_matches_type(SessionUpdateResponse, session, path=["response"]) @parametrize def test_raw_response_update(self, client: Browserbase) -> None: response = client.sessions.with_raw_response.update( - id="id", - project_id="projectId", + id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + project_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", status="REQUEST_RELEASE", ) assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" session = response.parse() - assert_matches_type(Session, session, path=["response"]) + assert_matches_type(SessionUpdateResponse, session, path=["response"]) @parametrize def test_streaming_response_update(self, client: Browserbase) -> None: with client.sessions.with_streaming_response.update( - id="id", - project_id="projectId", + id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + project_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", status="REQUEST_RELEASE", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" session = response.parse() - assert_matches_type(Session, session, path=["response"]) + assert_matches_type(SessionUpdateResponse, session, path=["response"]) assert cast(Any, response.is_closed) is True @@ -178,7 +188,7 @@ def test_path_params_update(self, client: Browserbase) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): client.sessions.with_raw_response.update( id="", - project_id="projectId", + project_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", status="REQUEST_RELEASE", ) @@ -218,31 +228,31 @@ def test_streaming_response_list(self, client: Browserbase) -> None: @parametrize def test_method_debug(self, client: Browserbase) -> None: session = client.sessions.debug( - "id", + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", ) - assert_matches_type(SessionLiveURLs, session, path=["response"]) + assert_matches_type(SessionDebugResponse, session, path=["response"]) @parametrize def test_raw_response_debug(self, client: Browserbase) -> None: response = client.sessions.with_raw_response.debug( - "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(SessionLiveURLs, session, path=["response"]) + assert_matches_type(SessionDebugResponse, session, path=["response"]) @parametrize def test_streaming_response_debug(self, client: Browserbase) -> None: with client.sessions.with_streaming_response.debug( - "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(SessionLiveURLs, session, path=["response"]) + assert_matches_type(SessionDebugResponse, session, path=["response"]) assert cast(Any, response.is_closed) is True @@ -262,24 +272,24 @@ class TestAsyncSessions: @parametrize async def test_method_create(self, async_client: AsyncBrowserbase) -> None: session = await async_client.sessions.create( - project_id="projectId", + project_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", ) assert_matches_type(SessionCreateResponse, session, path=["response"]) @parametrize async def test_method_create_with_all_params(self, async_client: AsyncBrowserbase) -> None: session = await async_client.sessions.create( - project_id="projectId", + project_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", browser_settings={ "advanced_stealth": True, "block_ads": True, "captcha_image_selector": "captchaImageSelector", "captcha_input_selector": "captchaInputSelector", "context": { - "id": "id", + "id": "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", "persist": True, }, - "extension_id": "extensionId", + "extension_id": "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", "fingerprint": { "browsers": ["chrome"], "devices": ["desktop"], @@ -301,9 +311,19 @@ async def test_method_create_with_all_params(self, async_client: AsyncBrowserbas "width": 0, }, }, - extension_id="extensionId", + extension_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", keep_alive=True, - proxies=True, + proxies=[ + { + "type": "browserbase", + "domain_pattern": "domainPattern", + "geolocation": { + "country": "xx", + "city": "city", + "state": "xx", + }, + } + ], region="us-west-2", api_timeout=60, user_metadata={"foo": "bar"}, @@ -313,7 +333,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncBrowserbas @parametrize async def test_raw_response_create(self, async_client: AsyncBrowserbase) -> None: response = await async_client.sessions.with_raw_response.create( - project_id="projectId", + project_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", ) assert response.is_closed is True @@ -324,7 +344,7 @@ async def test_raw_response_create(self, async_client: AsyncBrowserbase) -> None @parametrize async def test_streaming_response_create(self, async_client: AsyncBrowserbase) -> None: async with async_client.sessions.with_streaming_response.create( - project_id="projectId", + project_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -337,14 +357,14 @@ async def test_streaming_response_create(self, async_client: AsyncBrowserbase) - @parametrize async def test_method_retrieve(self, async_client: AsyncBrowserbase) -> None: session = await async_client.sessions.retrieve( - "id", + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", ) assert_matches_type(SessionRetrieveResponse, session, path=["response"]) @parametrize async def test_raw_response_retrieve(self, async_client: AsyncBrowserbase) -> None: response = await async_client.sessions.with_raw_response.retrieve( - "id", + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", ) assert response.is_closed is True @@ -355,7 +375,7 @@ async def test_raw_response_retrieve(self, async_client: AsyncBrowserbase) -> No @parametrize async def test_streaming_response_retrieve(self, async_client: AsyncBrowserbase) -> None: async with async_client.sessions.with_streaming_response.retrieve( - "id", + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -375,37 +395,37 @@ async def test_path_params_retrieve(self, async_client: AsyncBrowserbase) -> Non @parametrize async def test_method_update(self, async_client: AsyncBrowserbase) -> None: session = await async_client.sessions.update( - id="id", - project_id="projectId", + id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + project_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", status="REQUEST_RELEASE", ) - assert_matches_type(Session, session, path=["response"]) + assert_matches_type(SessionUpdateResponse, session, path=["response"]) @parametrize async def test_raw_response_update(self, async_client: AsyncBrowserbase) -> None: response = await async_client.sessions.with_raw_response.update( - id="id", - project_id="projectId", + id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + project_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", status="REQUEST_RELEASE", ) assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" session = await response.parse() - assert_matches_type(Session, session, path=["response"]) + assert_matches_type(SessionUpdateResponse, session, path=["response"]) @parametrize async def test_streaming_response_update(self, async_client: AsyncBrowserbase) -> None: async with async_client.sessions.with_streaming_response.update( - id="id", - project_id="projectId", + id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + project_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", status="REQUEST_RELEASE", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" session = await response.parse() - assert_matches_type(Session, session, path=["response"]) + assert_matches_type(SessionUpdateResponse, session, path=["response"]) assert cast(Any, response.is_closed) is True @@ -414,7 +434,7 @@ async def test_path_params_update(self, async_client: AsyncBrowserbase) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): await async_client.sessions.with_raw_response.update( id="", - project_id="projectId", + project_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", status="REQUEST_RELEASE", ) @@ -454,31 +474,31 @@ async def test_streaming_response_list(self, async_client: AsyncBrowserbase) -> @parametrize async def test_method_debug(self, async_client: AsyncBrowserbase) -> None: session = await async_client.sessions.debug( - "id", + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", ) - assert_matches_type(SessionLiveURLs, session, path=["response"]) + assert_matches_type(SessionDebugResponse, session, path=["response"]) @parametrize async def test_raw_response_debug(self, async_client: AsyncBrowserbase) -> None: response = await async_client.sessions.with_raw_response.debug( - "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(SessionLiveURLs, session, path=["response"]) + assert_matches_type(SessionDebugResponse, session, path=["response"]) @parametrize async def test_streaming_response_debug(self, async_client: AsyncBrowserbase) -> None: async with async_client.sessions.with_streaming_response.debug( - "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(SessionLiveURLs, session, path=["response"]) + assert_matches_type(SessionDebugResponse, session, path=["response"]) assert cast(Any, response.is_closed) is True diff --git a/tests/test_client.py b/tests/test_client.py index e94506ec..de86ad9e 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -727,7 +727,9 @@ def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter, clien respx_mock.post("/v1/sessions").mock(side_effect=httpx.TimeoutException("Test timeout error")) with pytest.raises(APITimeoutError): - client.sessions.with_streaming_response.create(project_id="projectId").__enter__() + client.sessions.with_streaming_response.create( + project_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e" + ).__enter__() assert _get_open_connections(self.client) == 0 @@ -737,7 +739,9 @@ def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter, client respx_mock.post("/v1/sessions").mock(return_value=httpx.Response(500)) with pytest.raises(APIStatusError): - client.sessions.with_streaming_response.create(project_id="projectId").__enter__() + client.sessions.with_streaming_response.create( + project_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e" + ).__enter__() assert _get_open_connections(self.client) == 0 @pytest.mark.parametrize("failures_before_success", [0, 2, 4]) @@ -766,7 +770,7 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: respx_mock.post("/v1/sessions").mock(side_effect=retry_handler) - response = client.sessions.with_raw_response.create(project_id="projectId") + response = client.sessions.with_raw_response.create(project_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e") assert response.retries_taken == failures_before_success assert int(response.http_request.headers.get("x-stainless-retry-count")) == failures_before_success @@ -791,7 +795,7 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: respx_mock.post("/v1/sessions").mock(side_effect=retry_handler) response = client.sessions.with_raw_response.create( - project_id="projectId", extra_headers={"x-stainless-retry-count": Omit()} + project_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", extra_headers={"x-stainless-retry-count": Omit()} ) assert len(response.http_request.headers.get_list("x-stainless-retry-count")) == 0 @@ -816,7 +820,7 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: respx_mock.post("/v1/sessions").mock(side_effect=retry_handler) response = client.sessions.with_raw_response.create( - project_id="projectId", extra_headers={"x-stainless-retry-count": "42"} + project_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", extra_headers={"x-stainless-retry-count": "42"} ) assert response.http_request.headers.get("x-stainless-retry-count") == "42" @@ -1548,7 +1552,9 @@ async def test_retrying_timeout_errors_doesnt_leak( respx_mock.post("/v1/sessions").mock(side_effect=httpx.TimeoutException("Test timeout error")) with pytest.raises(APITimeoutError): - await async_client.sessions.with_streaming_response.create(project_id="projectId").__aenter__() + await async_client.sessions.with_streaming_response.create( + project_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e" + ).__aenter__() assert _get_open_connections(self.client) == 0 @@ -1560,7 +1566,9 @@ async def test_retrying_status_errors_doesnt_leak( respx_mock.post("/v1/sessions").mock(return_value=httpx.Response(500)) with pytest.raises(APIStatusError): - await async_client.sessions.with_streaming_response.create(project_id="projectId").__aenter__() + await async_client.sessions.with_streaming_response.create( + project_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e" + ).__aenter__() assert _get_open_connections(self.client) == 0 @pytest.mark.parametrize("failures_before_success", [0, 2, 4]) @@ -1590,7 +1598,7 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: respx_mock.post("/v1/sessions").mock(side_effect=retry_handler) - response = await client.sessions.with_raw_response.create(project_id="projectId") + response = await client.sessions.with_raw_response.create(project_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e") assert response.retries_taken == failures_before_success assert int(response.http_request.headers.get("x-stainless-retry-count")) == failures_before_success @@ -1616,7 +1624,7 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: respx_mock.post("/v1/sessions").mock(side_effect=retry_handler) response = await client.sessions.with_raw_response.create( - project_id="projectId", extra_headers={"x-stainless-retry-count": Omit()} + project_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", extra_headers={"x-stainless-retry-count": Omit()} ) assert len(response.http_request.headers.get_list("x-stainless-retry-count")) == 0 @@ -1642,7 +1650,7 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: respx_mock.post("/v1/sessions").mock(side_effect=retry_handler) response = await client.sessions.with_raw_response.create( - project_id="projectId", extra_headers={"x-stainless-retry-count": "42"} + project_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", extra_headers={"x-stainless-retry-count": "42"} ) assert response.http_request.headers.get("x-stainless-retry-count") == "42" From 71316eddf73a9ee683818c02c47789e5a9203ff0 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 3 Jul 2025 02:15:01 +0000 Subject: [PATCH 159/330] chore(internal): codegen related update --- .github/workflows/ci.yml | 18 ++++++++++++++++-- scripts/utils/upload-artifact.sh | 12 +++++++----- 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7a4492ea..455b6dc7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -35,10 +35,10 @@ jobs: - name: Run lints run: ./scripts/lint - upload: + build: if: github.repository == 'stainless-sdks/browserbase-python' && (github.event_name == 'push' || github.event.pull_request.head.repo.fork) timeout-minutes: 10 - name: upload + name: build permissions: contents: read id-token: write @@ -46,6 +46,20 @@ 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 dependencies + run: rye sync --all-features + + - name: Run build + run: rye build + - name: Get GitHub OIDC Token id: github-oidc uses: actions/github-script@v6 diff --git a/scripts/utils/upload-artifact.sh b/scripts/utils/upload-artifact.sh index 7c3d028a..4fa57664 100755 --- a/scripts/utils/upload-artifact.sh +++ b/scripts/utils/upload-artifact.sh @@ -1,7 +1,9 @@ #!/usr/bin/env bash set -exuo pipefail -RESPONSE=$(curl -X POST "$URL" \ +FILENAME=$(basename dist/*.whl) + +RESPONSE=$(curl -X POST "$URL?filename=$FILENAME" \ -H "Authorization: Bearer $AUTH" \ -H "Content-Type: application/json") @@ -12,13 +14,13 @@ if [[ "$SIGNED_URL" == "null" ]]; then exit 1 fi -UPLOAD_RESPONSE=$(tar -cz . | curl -v -X PUT \ - -H "Content-Type: application/gzip" \ - --data-binary @- "$SIGNED_URL" 2>&1) +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/browserbase-python/$SHA'\033[0m" + echo -e "\033[32mInstallation: pip install 'https://pkg.stainless.com/s/browserbase-python/$SHA/$FILENAME'\033[0m" else echo -e "\033[31mFailed to upload artifact.\033[0m" exit 1 From 392e09afd9debff06168c8bca5997dd16917961e Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 3 Jul 2025 18:32:57 +0000 Subject: [PATCH 160/330] codegen metadata --- .stats.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index fd2e8eac..40aaa42c 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 18 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fbrowserbase-fe7af3b907d79ac271560c1d2e887ed741cfcc08cb8b75596094411a2091e223.yml -openapi_spec_hash: 999fb6ba05cd9be138ff94b787957ce9 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fbrowserbase-219341ea9864a23d33fbb51843fd6f762f41ec8be5154bd963bfceff0bc30bb1.yml +openapi_spec_hash: 43fdb5f9ab7c52a17206c881128afb45 config_hash: b3ca4ec5b02e5333af51ebc2e9fdef1b From 381d8888623124d428047e8370a5d68fe38277fc Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 7 Jul 2025 03:10:30 +0000 Subject: [PATCH 161/330] feat(api): api update --- requirements-dev.lock | 2 +- requirements.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements-dev.lock b/requirements-dev.lock index 97a18b3f..9b3d89d6 100644 --- a/requirements-dev.lock +++ b/requirements-dev.lock @@ -56,7 +56,7 @@ httpx==0.28.1 # via browserbase # via httpx-aiohttp # via respx -httpx-aiohttp==0.1.6 +httpx-aiohttp==0.1.8 # via browserbase idna==3.4 # via anyio diff --git a/requirements.lock b/requirements.lock index 5a5c248f..091cafd6 100644 --- a/requirements.lock +++ b/requirements.lock @@ -43,7 +43,7 @@ httpcore==1.0.2 httpx==0.28.1 # via browserbase # via httpx-aiohttp -httpx-aiohttp==0.1.6 +httpx-aiohttp==0.1.8 # via browserbase idna==3.4 # via anyio From 89ebbf6e831f151e055e7a27a472662ecba7d151 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 9 Jul 2025 02:35:03 +0000 Subject: [PATCH 162/330] chore(internal): bump pinned h11 dep --- requirements-dev.lock | 4 ++-- requirements.lock | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/requirements-dev.lock b/requirements-dev.lock index 9b3d89d6..ee261e52 100644 --- a/requirements-dev.lock +++ b/requirements-dev.lock @@ -48,9 +48,9 @@ filelock==3.12.4 frozenlist==1.6.2 # via aiohttp # via aiosignal -h11==0.14.0 +h11==0.16.0 # via httpcore -httpcore==1.0.2 +httpcore==1.0.9 # via httpx httpx==0.28.1 # via browserbase diff --git a/requirements.lock b/requirements.lock index 091cafd6..6f4c4c9e 100644 --- a/requirements.lock +++ b/requirements.lock @@ -36,9 +36,9 @@ exceptiongroup==1.2.2 frozenlist==1.6.2 # via aiohttp # via aiosignal -h11==0.14.0 +h11==0.16.0 # via httpcore -httpcore==1.0.2 +httpcore==1.0.9 # via httpx httpx==0.28.1 # via browserbase From 7644c24cf0aac3d8aa57fc61d632afaba683daa8 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 9 Jul 2025 02:54:58 +0000 Subject: [PATCH 163/330] chore(package): mark python 3.13 as supported --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index b0bd0a87..2c2cc6d2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,6 +24,7 @@ classifiers = [ "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", "Operating System :: OS Independent", "Operating System :: POSIX", "Operating System :: MacOS", From f756a15ca9bbbdb6e643f2a178b897024485c427 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 10 Jul 2025 02:49:53 +0000 Subject: [PATCH 164/330] fix(parsing): correctly handle nested discriminated unions --- src/browserbase/_models.py | 13 ++++++----- tests/test_models.py | 45 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 5 deletions(-) diff --git a/src/browserbase/_models.py b/src/browserbase/_models.py index 4f214980..528d5680 100644 --- a/src/browserbase/_models.py +++ b/src/browserbase/_models.py @@ -2,9 +2,10 @@ import os import inspect -from typing import TYPE_CHECKING, Any, Type, Union, Generic, TypeVar, Callable, cast +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, @@ -366,7 +367,7 @@ def _construct_field(value: object, field: FieldInfo, key: str) -> object: if type_ is None: raise RuntimeError(f"Unexpected field type is None for {key}") - return construct_type(value=value, type_=type_) + return construct_type(value=value, type_=type_, metadata=getattr(field, "metadata", None)) def is_basemodel(type_: type) -> bool: @@ -420,7 +421,7 @@ def construct_type_unchecked(*, value: object, type_: type[_T]) -> _T: return cast(_T, construct_type(value=value, type_=type_)) -def construct_type(*, value: object, type_: object) -> object: +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. @@ -438,8 +439,10 @@ def construct_type(*, value: object, type_: object) -> object: type_ = type_.__value__ # type: ignore[unreachable] # unwrap `Annotated[T, ...]` -> `T` - if is_annotated_type(type_): - meta: tuple[Any, ...] = get_args(type_)[1:] + if metadata is not None: + meta: tuple[Any, ...] = tuple(metadata) + elif is_annotated_type(type_): + meta = get_args(type_)[1:] type_ = extract_type_arg(type_, 0) else: meta = tuple() diff --git a/tests/test_models.py b/tests/test_models.py index b5335f94..51fabb73 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -889,3 +889,48 @@ class ModelB(BaseModel): ) 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) From 144efc3e91a5fe550bc83f9950f55639791dd8e1 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 10 Jul 2025 02:50:12 +0000 Subject: [PATCH 165/330] feat(api): api update --- src/browserbase/_models.py | 13 +++++------ tests/test_models.py | 45 -------------------------------------- 2 files changed, 5 insertions(+), 53 deletions(-) diff --git a/src/browserbase/_models.py b/src/browserbase/_models.py index 528d5680..4f214980 100644 --- a/src/browserbase/_models.py +++ b/src/browserbase/_models.py @@ -2,10 +2,9 @@ import os import inspect -from typing import TYPE_CHECKING, Any, Type, Union, Generic, TypeVar, Callable, Optional, cast +from typing import TYPE_CHECKING, Any, Type, Union, Generic, TypeVar, Callable, cast from datetime import date, datetime from typing_extensions import ( - List, Unpack, Literal, ClassVar, @@ -367,7 +366,7 @@ def _construct_field(value: object, field: FieldInfo, key: str) -> object: 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)) + return construct_type(value=value, type_=type_) def is_basemodel(type_: type) -> bool: @@ -421,7 +420,7 @@ def construct_type_unchecked(*, value: object, type_: type[_T]) -> _T: return cast(_T, construct_type(value=value, type_=type_)) -def construct_type(*, value: object, type_: object, metadata: Optional[List[Any]] = None) -> object: +def construct_type(*, value: object, type_: object) -> 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. @@ -439,10 +438,8 @@ def construct_type(*, value: object, type_: object, metadata: Optional[List[Any] type_ = type_.__value__ # type: ignore[unreachable] # unwrap `Annotated[T, ...]` -> `T` - if metadata is not None: - meta: tuple[Any, ...] = tuple(metadata) - elif is_annotated_type(type_): - meta = get_args(type_)[1:] + if is_annotated_type(type_): + meta: tuple[Any, ...] = get_args(type_)[1:] type_ = extract_type_arg(type_, 0) else: meta = tuple() diff --git a/tests/test_models.py b/tests/test_models.py index 51fabb73..b5335f94 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -889,48 +889,3 @@ class ModelB(BaseModel): ) 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) From 4352c2f964c1734a7514843ba9feb7f2680056a7 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 10 Jul 2025 03:08:31 +0000 Subject: [PATCH 166/330] chore(internal): codegen related update --- src/browserbase/_models.py | 13 ++++++----- tests/test_models.py | 45 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 5 deletions(-) diff --git a/src/browserbase/_models.py b/src/browserbase/_models.py index 4f214980..528d5680 100644 --- a/src/browserbase/_models.py +++ b/src/browserbase/_models.py @@ -2,9 +2,10 @@ import os import inspect -from typing import TYPE_CHECKING, Any, Type, Union, Generic, TypeVar, Callable, cast +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, @@ -366,7 +367,7 @@ def _construct_field(value: object, field: FieldInfo, key: str) -> object: if type_ is None: raise RuntimeError(f"Unexpected field type is None for {key}") - return construct_type(value=value, type_=type_) + return construct_type(value=value, type_=type_, metadata=getattr(field, "metadata", None)) def is_basemodel(type_: type) -> bool: @@ -420,7 +421,7 @@ def construct_type_unchecked(*, value: object, type_: type[_T]) -> _T: return cast(_T, construct_type(value=value, type_=type_)) -def construct_type(*, value: object, type_: object) -> object: +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. @@ -438,8 +439,10 @@ def construct_type(*, value: object, type_: object) -> object: type_ = type_.__value__ # type: ignore[unreachable] # unwrap `Annotated[T, ...]` -> `T` - if is_annotated_type(type_): - meta: tuple[Any, ...] = get_args(type_)[1:] + if metadata is not None: + meta: tuple[Any, ...] = tuple(metadata) + elif is_annotated_type(type_): + meta = get_args(type_)[1:] type_ = extract_type_arg(type_, 0) else: meta = tuple() diff --git a/tests/test_models.py b/tests/test_models.py index b5335f94..51fabb73 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -889,3 +889,48 @@ class ModelB(BaseModel): ) 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) From e5e08cdc22018f290598122227f3ed8329d5a6f8 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 10 Jul 2025 03:33:39 +0000 Subject: [PATCH 167/330] feat(api): api update --- src/browserbase/_models.py | 13 +++++------ tests/test_models.py | 45 -------------------------------------- 2 files changed, 5 insertions(+), 53 deletions(-) diff --git a/src/browserbase/_models.py b/src/browserbase/_models.py index 528d5680..4f214980 100644 --- a/src/browserbase/_models.py +++ b/src/browserbase/_models.py @@ -2,10 +2,9 @@ import os import inspect -from typing import TYPE_CHECKING, Any, Type, Union, Generic, TypeVar, Callable, Optional, cast +from typing import TYPE_CHECKING, Any, Type, Union, Generic, TypeVar, Callable, cast from datetime import date, datetime from typing_extensions import ( - List, Unpack, Literal, ClassVar, @@ -367,7 +366,7 @@ def _construct_field(value: object, field: FieldInfo, key: str) -> object: 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)) + return construct_type(value=value, type_=type_) def is_basemodel(type_: type) -> bool: @@ -421,7 +420,7 @@ def construct_type_unchecked(*, value: object, type_: type[_T]) -> _T: return cast(_T, construct_type(value=value, type_=type_)) -def construct_type(*, value: object, type_: object, metadata: Optional[List[Any]] = None) -> object: +def construct_type(*, value: object, type_: object) -> 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. @@ -439,10 +438,8 @@ def construct_type(*, value: object, type_: object, metadata: Optional[List[Any] type_ = type_.__value__ # type: ignore[unreachable] # unwrap `Annotated[T, ...]` -> `T` - if metadata is not None: - meta: tuple[Any, ...] = tuple(metadata) - elif is_annotated_type(type_): - meta = get_args(type_)[1:] + if is_annotated_type(type_): + meta: tuple[Any, ...] = get_args(type_)[1:] type_ = extract_type_arg(type_, 0) else: meta = tuple() diff --git a/tests/test_models.py b/tests/test_models.py index 51fabb73..b5335f94 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -889,48 +889,3 @@ class ModelB(BaseModel): ) 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) From ecdbe567758e4312c79dff1ffcb9f35173756a04 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 10 Jul 2025 03:51:45 +0000 Subject: [PATCH 168/330] chore(internal): codegen related update --- src/browserbase/_models.py | 13 ++++++----- tests/test_models.py | 45 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 5 deletions(-) diff --git a/src/browserbase/_models.py b/src/browserbase/_models.py index 4f214980..528d5680 100644 --- a/src/browserbase/_models.py +++ b/src/browserbase/_models.py @@ -2,9 +2,10 @@ import os import inspect -from typing import TYPE_CHECKING, Any, Type, Union, Generic, TypeVar, Callable, cast +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, @@ -366,7 +367,7 @@ def _construct_field(value: object, field: FieldInfo, key: str) -> object: if type_ is None: raise RuntimeError(f"Unexpected field type is None for {key}") - return construct_type(value=value, type_=type_) + return construct_type(value=value, type_=type_, metadata=getattr(field, "metadata", None)) def is_basemodel(type_: type) -> bool: @@ -420,7 +421,7 @@ def construct_type_unchecked(*, value: object, type_: type[_T]) -> _T: return cast(_T, construct_type(value=value, type_=type_)) -def construct_type(*, value: object, type_: object) -> object: +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. @@ -438,8 +439,10 @@ def construct_type(*, value: object, type_: object) -> object: type_ = type_.__value__ # type: ignore[unreachable] # unwrap `Annotated[T, ...]` -> `T` - if is_annotated_type(type_): - meta: tuple[Any, ...] = get_args(type_)[1:] + if metadata is not None: + meta: tuple[Any, ...] = tuple(metadata) + elif is_annotated_type(type_): + meta = get_args(type_)[1:] type_ = extract_type_arg(type_, 0) else: meta = tuple() diff --git a/tests/test_models.py b/tests/test_models.py index b5335f94..51fabb73 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -889,3 +889,48 @@ class ModelB(BaseModel): ) 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) From e5ff8eb86998c471827c1c2ac770a463eabd633b Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 10 Jul 2025 11:12:25 +0000 Subject: [PATCH 169/330] feat(api): api update --- src/browserbase/_models.py | 13 +++++------ tests/test_models.py | 45 -------------------------------------- 2 files changed, 5 insertions(+), 53 deletions(-) diff --git a/src/browserbase/_models.py b/src/browserbase/_models.py index 528d5680..4f214980 100644 --- a/src/browserbase/_models.py +++ b/src/browserbase/_models.py @@ -2,10 +2,9 @@ import os import inspect -from typing import TYPE_CHECKING, Any, Type, Union, Generic, TypeVar, Callable, Optional, cast +from typing import TYPE_CHECKING, Any, Type, Union, Generic, TypeVar, Callable, cast from datetime import date, datetime from typing_extensions import ( - List, Unpack, Literal, ClassVar, @@ -367,7 +366,7 @@ def _construct_field(value: object, field: FieldInfo, key: str) -> object: 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)) + return construct_type(value=value, type_=type_) def is_basemodel(type_: type) -> bool: @@ -421,7 +420,7 @@ def construct_type_unchecked(*, value: object, type_: type[_T]) -> _T: return cast(_T, construct_type(value=value, type_=type_)) -def construct_type(*, value: object, type_: object, metadata: Optional[List[Any]] = None) -> object: +def construct_type(*, value: object, type_: object) -> 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. @@ -439,10 +438,8 @@ def construct_type(*, value: object, type_: object, metadata: Optional[List[Any] type_ = type_.__value__ # type: ignore[unreachable] # unwrap `Annotated[T, ...]` -> `T` - if metadata is not None: - meta: tuple[Any, ...] = tuple(metadata) - elif is_annotated_type(type_): - meta = get_args(type_)[1:] + if is_annotated_type(type_): + meta: tuple[Any, ...] = get_args(type_)[1:] type_ = extract_type_arg(type_, 0) else: meta = tuple() diff --git a/tests/test_models.py b/tests/test_models.py index 51fabb73..b5335f94 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -889,48 +889,3 @@ class ModelB(BaseModel): ) 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) From 6d1899bc173aff580e43e90122fe9d6daa9c2604 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 11 Jul 2025 02:14:19 +0000 Subject: [PATCH 170/330] chore(internal): codegen related update --- src/browserbase/_models.py | 13 ++++++----- tests/test_models.py | 45 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 5 deletions(-) diff --git a/src/browserbase/_models.py b/src/browserbase/_models.py index 4f214980..528d5680 100644 --- a/src/browserbase/_models.py +++ b/src/browserbase/_models.py @@ -2,9 +2,10 @@ import os import inspect -from typing import TYPE_CHECKING, Any, Type, Union, Generic, TypeVar, Callable, cast +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, @@ -366,7 +367,7 @@ def _construct_field(value: object, field: FieldInfo, key: str) -> object: if type_ is None: raise RuntimeError(f"Unexpected field type is None for {key}") - return construct_type(value=value, type_=type_) + return construct_type(value=value, type_=type_, metadata=getattr(field, "metadata", None)) def is_basemodel(type_: type) -> bool: @@ -420,7 +421,7 @@ def construct_type_unchecked(*, value: object, type_: type[_T]) -> _T: return cast(_T, construct_type(value=value, type_=type_)) -def construct_type(*, value: object, type_: object) -> object: +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. @@ -438,8 +439,10 @@ def construct_type(*, value: object, type_: object) -> object: type_ = type_.__value__ # type: ignore[unreachable] # unwrap `Annotated[T, ...]` -> `T` - if is_annotated_type(type_): - meta: tuple[Any, ...] = get_args(type_)[1:] + if metadata is not None: + meta: tuple[Any, ...] = tuple(metadata) + elif is_annotated_type(type_): + meta = get_args(type_)[1:] type_ = extract_type_arg(type_, 0) else: meta = tuple() diff --git a/tests/test_models.py b/tests/test_models.py index b5335f94..51fabb73 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -889,3 +889,48 @@ class ModelB(BaseModel): ) 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) From 10e4cfc508f03f39021fa313829752ee86c044a0 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 11 Jul 2025 03:08:39 +0000 Subject: [PATCH 171/330] chore(readme): fix version rendering on pypi --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 4e4ed1d6..3fc046e1 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ # Browserbase Python API library -[![PyPI version]()](https://pypi.org/project/browserbase/) + +[![PyPI version](https://img.shields.io/pypi/v/browserbase.svg?label=pypi%20(stable))](https://pypi.org/project/browserbase/) The Browserbase Python library provides convenient access to the Browserbase REST API from any Python 3.8+ application. The library includes type definitions for all request params and response fields, From 9589077414670e38ec65f092b1aa090362088d1c Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 11 Jul 2025 12:36:49 +0000 Subject: [PATCH 172/330] feat(api): api update --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index 3fc046e1..4e4ed1d6 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,6 @@ # Browserbase Python API library - -[![PyPI version](https://img.shields.io/pypi/v/browserbase.svg?label=pypi%20(stable))](https://pypi.org/project/browserbase/) +[![PyPI version]()](https://pypi.org/project/browserbase/) The Browserbase Python library provides convenient access to the Browserbase REST API from any Python 3.8+ application. The library includes type definitions for all request params and response fields, From 9a9220bc3461d0925e41d9f7ee0c6f0765fc5f9f Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 12 Jul 2025 02:14:43 +0000 Subject: [PATCH 173/330] fix(client): don't send Content-Type header on GET requests --- README.md | 3 ++- pyproject.toml | 2 +- src/browserbase/_base_client.py | 11 +++++++++-- tests/test_client.py | 4 ++-- 4 files changed, 14 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 4e4ed1d6..3fc046e1 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ # Browserbase Python API library -[![PyPI version]()](https://pypi.org/project/browserbase/) + +[![PyPI version](https://img.shields.io/pypi/v/browserbase.svg?label=pypi%20(stable))](https://pypi.org/project/browserbase/) The Browserbase Python library provides convenient access to the Browserbase REST API from any Python 3.8+ application. The library includes type definitions for all request params and response fields, diff --git a/pyproject.toml b/pyproject.toml index 2c2cc6d2..84bbf0a2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,7 +39,7 @@ Homepage = "https://github.com/browserbase/sdk-python" Repository = "https://github.com/browserbase/sdk-python" [project.optional-dependencies] -aiohttp = ["aiohttp", "httpx_aiohttp>=0.1.6"] +aiohttp = ["aiohttp", "httpx_aiohttp>=0.1.8"] [tool.rye] managed = true diff --git a/src/browserbase/_base_client.py b/src/browserbase/_base_client.py index e191446e..434b1e8c 100644 --- a/src/browserbase/_base_client.py +++ b/src/browserbase/_base_client.py @@ -529,6 +529,15 @@ def _build_request( # 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: + 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, @@ -540,8 +549,6 @@ def _build_request( # 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, - json=json_data if is_given(json_data) else None, - files=files, **kwargs, ) diff --git a/tests/test_client.py b/tests/test_client.py index de86ad9e..1e26547d 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -464,7 +464,7 @@ def test_request_extra_query(self) -> None: def test_multipart_repeating_array(self, client: Browserbase) -> None: request = client._build_request( FinalRequestOptions.construct( - method="get", + method="post", url="/foo", headers={"Content-Type": "multipart/form-data; boundary=6b7ba517decee4a450543ea6ae821c82"}, json_data={"array": ["foo", "bar"]}, @@ -1283,7 +1283,7 @@ def test_request_extra_query(self) -> None: def test_multipart_repeating_array(self, async_client: AsyncBrowserbase) -> None: request = async_client._build_request( FinalRequestOptions.construct( - method="get", + method="post", url="/foo", headers={"Content-Type": "multipart/form-data; boundary=6b7ba517decee4a450543ea6ae821c82"}, json_data={"array": ["foo", "bar"]}, From 4b293e1a83134300892389df0b268328dbc7570a Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 15 Jul 2025 02:14:08 +0000 Subject: [PATCH 174/330] feat: clean up environment call outs --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index 3fc046e1..21e79b5a 100644 --- a/README.md +++ b/README.md @@ -83,7 +83,6 @@ pip install browserbase[aiohttp] Then you can enable it by instantiating the client with `http_client=DefaultAioHttpClient()`: ```python -import os import asyncio from browserbase import DefaultAioHttpClient from browserbase import AsyncBrowserbase @@ -91,7 +90,7 @@ from browserbase import AsyncBrowserbase async def main() -> None: async with AsyncBrowserbase( - api_key=os.environ.get("BROWSERBASE_API_KEY"), # This is the default and can be omitted + api_key="My API Key", http_client=DefaultAioHttpClient(), ) as client: session = await client.sessions.create( From 1ab901e3a7671a62306ecaf529f6a7667fdef6c8 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 16 Jul 2025 23:03:12 +0000 Subject: [PATCH 175/330] feat(api): api update --- .stats.yml | 4 ++-- src/browserbase/types/session_create_params.py | 12 ++++++++---- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/.stats.yml b/.stats.yml index 40aaa42c..b1f68ad6 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 18 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fbrowserbase-219341ea9864a23d33fbb51843fd6f762f41ec8be5154bd963bfceff0bc30bb1.yml -openapi_spec_hash: 43fdb5f9ab7c52a17206c881128afb45 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fbrowserbase-86464130af6afb678b92cd7a412035fa95d0f806eb35d5cfc1902c0d417c44ca.yml +openapi_spec_hash: 9df21af9ca1497c2a481936ba585290d config_hash: b3ca4ec5b02e5333af51ebc2e9fdef1b diff --git a/src/browserbase/types/session_create_params.py b/src/browserbase/types/session_create_params.py index a507f903..f3c49179 100644 --- a/src/browserbase/types/session_create_params.py +++ b/src/browserbase/types/session_create_params.py @@ -74,13 +74,13 @@ class BrowserSettingsContext(TypedDict, total=False): class BrowserSettingsFingerprintScreen(TypedDict, total=False): - max_height: Required[Annotated[int, PropertyInfo(alias="maxHeight")]] + max_height: Annotated[int, PropertyInfo(alias="maxHeight")] - max_width: Required[Annotated[int, PropertyInfo(alias="maxWidth")]] + max_width: Annotated[int, PropertyInfo(alias="maxWidth")] - min_height: Required[Annotated[int, PropertyInfo(alias="minHeight")]] + min_height: Annotated[int, PropertyInfo(alias="minHeight")] - min_width: Required[Annotated[int, PropertyInfo(alias="minWidth")]] + min_width: Annotated[int, PropertyInfo(alias="minWidth")] class BrowserSettingsFingerprint(TypedDict, total=False): @@ -135,6 +135,10 @@ class BrowserSettings(TypedDict, total=False): """ fingerprint: BrowserSettingsFingerprint + """ + See usage examples + [on the Stealth Mode page](/features/stealth-mode#fingerprinting) + """ log_session: Annotated[bool, PropertyInfo(alias="logSession")] """Enable or disable session logging. Defaults to `true`.""" From 50bf97d6a8eddaa6dadf161401dc640acda72454 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 17 Jul 2025 16:44:50 +0000 Subject: [PATCH 176/330] feat(api): api update --- .stats.yml | 4 +- README.md | 2 +- .../api_resources/sessions/test_downloads.py | 36 +++------ tests/api_resources/sessions/test_logs.py | 12 +-- .../api_resources/sessions/test_recording.py | 12 +-- tests/api_resources/sessions/test_uploads.py | 12 +-- tests/api_resources/test_contexts.py | 36 ++++----- tests/api_resources/test_extensions.py | 24 +++--- tests/api_resources/test_projects.py | 24 +++--- tests/api_resources/test_sessions.py | 80 +++++++++---------- tests/test_client.py | 28 +++---- 11 files changed, 125 insertions(+), 145 deletions(-) diff --git a/.stats.yml b/.stats.yml index b1f68ad6..772d8de0 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 18 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fbrowserbase-86464130af6afb678b92cd7a412035fa95d0f806eb35d5cfc1902c0d417c44ca.yml -openapi_spec_hash: 9df21af9ca1497c2a481936ba585290d +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fbrowserbase-3d350e6cd04452a1654fdb7a93fa7e8dbbf7706273ae7c21818efce9dcf9bbfe.yml +openapi_spec_hash: 25beffd2761e5414d0cb32f74a969a38 config_hash: b3ca4ec5b02e5333af51ebc2e9fdef1b diff --git a/README.md b/README.md index 21e79b5a..726c766d 100644 --- a/README.md +++ b/README.md @@ -121,7 +121,7 @@ from browserbase import Browserbase client = Browserbase() session = client.sessions.create( - project_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + project_id="projectId", browser_settings={}, ) print(session.browser_settings) diff --git a/tests/api_resources/sessions/test_downloads.py b/tests/api_resources/sessions/test_downloads.py index ed2feb9f..10e84fdb 100644 --- a/tests/api_resources/sessions/test_downloads.py +++ b/tests/api_resources/sessions/test_downloads.py @@ -26,11 +26,9 @@ class TestDownloads: @parametrize @pytest.mark.respx(base_url=base_url) def test_method_list(self, client: Browserbase, respx_mock: MockRouter) -> None: - respx_mock.get("/v1/sessions/182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e/downloads").mock( - return_value=httpx.Response(200, json={"foo": "bar"}) - ) + respx_mock.get("/v1/sessions/id/downloads").mock(return_value=httpx.Response(200, json={"foo": "bar"})) download = client.sessions.downloads.list( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + "id", ) assert download.is_closed assert download.json() == {"foo": "bar"} @@ -40,12 +38,10 @@ def test_method_list(self, client: Browserbase, respx_mock: MockRouter) -> None: @parametrize @pytest.mark.respx(base_url=base_url) def test_raw_response_list(self, client: Browserbase, respx_mock: MockRouter) -> None: - respx_mock.get("/v1/sessions/182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e/downloads").mock( - return_value=httpx.Response(200, json={"foo": "bar"}) - ) + respx_mock.get("/v1/sessions/id/downloads").mock(return_value=httpx.Response(200, json={"foo": "bar"})) download = client.sessions.downloads.with_raw_response.list( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + "id", ) assert download.is_closed is True @@ -56,11 +52,9 @@ def test_raw_response_list(self, client: Browserbase, respx_mock: MockRouter) -> @parametrize @pytest.mark.respx(base_url=base_url) def test_streaming_response_list(self, client: Browserbase, respx_mock: MockRouter) -> None: - respx_mock.get("/v1/sessions/182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e/downloads").mock( - return_value=httpx.Response(200, json={"foo": "bar"}) - ) + respx_mock.get("/v1/sessions/id/downloads").mock(return_value=httpx.Response(200, json={"foo": "bar"})) with client.sessions.downloads.with_streaming_response.list( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + "id", ) as download: assert not download.is_closed assert download.http_request.headers.get("X-Stainless-Lang") == "python" @@ -88,11 +82,9 @@ class TestAsyncDownloads: @parametrize @pytest.mark.respx(base_url=base_url) async def test_method_list(self, async_client: AsyncBrowserbase, respx_mock: MockRouter) -> None: - respx_mock.get("/v1/sessions/182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e/downloads").mock( - return_value=httpx.Response(200, json={"foo": "bar"}) - ) + respx_mock.get("/v1/sessions/id/downloads").mock(return_value=httpx.Response(200, json={"foo": "bar"})) download = await async_client.sessions.downloads.list( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + "id", ) assert download.is_closed assert await download.json() == {"foo": "bar"} @@ -102,12 +94,10 @@ async def test_method_list(self, async_client: AsyncBrowserbase, respx_mock: Moc @parametrize @pytest.mark.respx(base_url=base_url) async def test_raw_response_list(self, async_client: AsyncBrowserbase, respx_mock: MockRouter) -> None: - respx_mock.get("/v1/sessions/182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e/downloads").mock( - return_value=httpx.Response(200, json={"foo": "bar"}) - ) + respx_mock.get("/v1/sessions/id/downloads").mock(return_value=httpx.Response(200, json={"foo": "bar"})) download = await async_client.sessions.downloads.with_raw_response.list( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + "id", ) assert download.is_closed is True @@ -118,11 +108,9 @@ async def test_raw_response_list(self, async_client: AsyncBrowserbase, respx_moc @parametrize @pytest.mark.respx(base_url=base_url) async def test_streaming_response_list(self, async_client: AsyncBrowserbase, respx_mock: MockRouter) -> None: - respx_mock.get("/v1/sessions/182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e/downloads").mock( - return_value=httpx.Response(200, json={"foo": "bar"}) - ) + respx_mock.get("/v1/sessions/id/downloads").mock(return_value=httpx.Response(200, json={"foo": "bar"})) async with async_client.sessions.downloads.with_streaming_response.list( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + "id", ) as download: assert not download.is_closed assert download.http_request.headers.get("X-Stainless-Lang") == "python" diff --git a/tests/api_resources/sessions/test_logs.py b/tests/api_resources/sessions/test_logs.py index 96d4779e..eadde723 100644 --- a/tests/api_resources/sessions/test_logs.py +++ b/tests/api_resources/sessions/test_logs.py @@ -20,14 +20,14 @@ class TestLogs: @parametrize def test_method_list(self, client: Browserbase) -> None: log = client.sessions.logs.list( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + "id", ) assert_matches_type(LogListResponse, log, path=["response"]) @parametrize def test_raw_response_list(self, client: Browserbase) -> None: response = client.sessions.logs.with_raw_response.list( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + "id", ) assert response.is_closed is True @@ -38,7 +38,7 @@ def test_raw_response_list(self, client: Browserbase) -> None: @parametrize def test_streaming_response_list(self, client: Browserbase) -> None: with client.sessions.logs.with_streaming_response.list( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + "id", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -64,14 +64,14 @@ class TestAsyncLogs: @parametrize async def test_method_list(self, async_client: AsyncBrowserbase) -> None: log = await async_client.sessions.logs.list( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + "id", ) assert_matches_type(LogListResponse, log, path=["response"]) @parametrize async def test_raw_response_list(self, async_client: AsyncBrowserbase) -> None: response = await async_client.sessions.logs.with_raw_response.list( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + "id", ) assert response.is_closed is True @@ -82,7 +82,7 @@ async def test_raw_response_list(self, async_client: AsyncBrowserbase) -> None: @parametrize async def test_streaming_response_list(self, async_client: AsyncBrowserbase) -> None: async with async_client.sessions.logs.with_streaming_response.list( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + "id", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" diff --git a/tests/api_resources/sessions/test_recording.py b/tests/api_resources/sessions/test_recording.py index c60c0c1b..f1e97d07 100644 --- a/tests/api_resources/sessions/test_recording.py +++ b/tests/api_resources/sessions/test_recording.py @@ -20,14 +20,14 @@ class TestRecording: @parametrize def test_method_retrieve(self, client: Browserbase) -> None: recording = client.sessions.recording.retrieve( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + "id", ) assert_matches_type(RecordingRetrieveResponse, recording, path=["response"]) @parametrize def test_raw_response_retrieve(self, client: Browserbase) -> None: response = client.sessions.recording.with_raw_response.retrieve( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + "id", ) assert response.is_closed is True @@ -38,7 +38,7 @@ def test_raw_response_retrieve(self, client: Browserbase) -> None: @parametrize def test_streaming_response_retrieve(self, client: Browserbase) -> None: with client.sessions.recording.with_streaming_response.retrieve( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + "id", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -64,14 +64,14 @@ class TestAsyncRecording: @parametrize async def test_method_retrieve(self, async_client: AsyncBrowserbase) -> None: recording = await async_client.sessions.recording.retrieve( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + "id", ) assert_matches_type(RecordingRetrieveResponse, recording, path=["response"]) @parametrize async def test_raw_response_retrieve(self, async_client: AsyncBrowserbase) -> None: response = await async_client.sessions.recording.with_raw_response.retrieve( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + "id", ) assert response.is_closed is True @@ -82,7 +82,7 @@ async def test_raw_response_retrieve(self, async_client: AsyncBrowserbase) -> No @parametrize async def test_streaming_response_retrieve(self, async_client: AsyncBrowserbase) -> None: async with async_client.sessions.recording.with_streaming_response.retrieve( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + "id", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" diff --git a/tests/api_resources/sessions/test_uploads.py b/tests/api_resources/sessions/test_uploads.py index 0a8b0fae..748b92e7 100644 --- a/tests/api_resources/sessions/test_uploads.py +++ b/tests/api_resources/sessions/test_uploads.py @@ -20,7 +20,7 @@ class TestUploads: @parametrize def test_method_create(self, client: Browserbase) -> None: upload = client.sessions.uploads.create( - id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + id="id", file=b"raw file contents", ) assert_matches_type(UploadCreateResponse, upload, path=["response"]) @@ -28,7 +28,7 @@ def test_method_create(self, client: Browserbase) -> None: @parametrize def test_raw_response_create(self, client: Browserbase) -> None: response = client.sessions.uploads.with_raw_response.create( - id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + id="id", file=b"raw file contents", ) @@ -40,7 +40,7 @@ def test_raw_response_create(self, client: Browserbase) -> None: @parametrize def test_streaming_response_create(self, client: Browserbase) -> None: with client.sessions.uploads.with_streaming_response.create( - id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + id="id", file=b"raw file contents", ) as response: assert not response.is_closed @@ -68,7 +68,7 @@ class TestAsyncUploads: @parametrize async def test_method_create(self, async_client: AsyncBrowserbase) -> None: upload = await async_client.sessions.uploads.create( - id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + id="id", file=b"raw file contents", ) assert_matches_type(UploadCreateResponse, upload, path=["response"]) @@ -76,7 +76,7 @@ async def test_method_create(self, async_client: AsyncBrowserbase) -> None: @parametrize async def test_raw_response_create(self, async_client: AsyncBrowserbase) -> None: response = await async_client.sessions.uploads.with_raw_response.create( - id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + id="id", file=b"raw file contents", ) @@ -88,7 +88,7 @@ async def test_raw_response_create(self, async_client: AsyncBrowserbase) -> None @parametrize async def test_streaming_response_create(self, async_client: AsyncBrowserbase) -> None: async with async_client.sessions.uploads.with_streaming_response.create( - id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + id="id", file=b"raw file contents", ) as response: assert not response.is_closed diff --git a/tests/api_resources/test_contexts.py b/tests/api_resources/test_contexts.py index d977efb2..4ad27733 100644 --- a/tests/api_resources/test_contexts.py +++ b/tests/api_resources/test_contexts.py @@ -24,14 +24,14 @@ class TestContexts: @parametrize def test_method_create(self, client: Browserbase) -> None: context = client.contexts.create( - project_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + project_id="projectId", ) assert_matches_type(ContextCreateResponse, context, path=["response"]) @parametrize def test_raw_response_create(self, client: Browserbase) -> None: response = client.contexts.with_raw_response.create( - project_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + project_id="projectId", ) assert response.is_closed is True @@ -42,7 +42,7 @@ def test_raw_response_create(self, client: Browserbase) -> None: @parametrize def test_streaming_response_create(self, client: Browserbase) -> None: with client.contexts.with_streaming_response.create( - project_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + project_id="projectId", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -55,14 +55,14 @@ def test_streaming_response_create(self, client: Browserbase) -> None: @parametrize def test_method_retrieve(self, client: Browserbase) -> None: context = client.contexts.retrieve( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + "id", ) assert_matches_type(ContextRetrieveResponse, context, path=["response"]) @parametrize def test_raw_response_retrieve(self, client: Browserbase) -> None: response = client.contexts.with_raw_response.retrieve( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + "id", ) assert response.is_closed is True @@ -73,7 +73,7 @@ def test_raw_response_retrieve(self, client: Browserbase) -> None: @parametrize def test_streaming_response_retrieve(self, client: Browserbase) -> None: with client.contexts.with_streaming_response.retrieve( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + "id", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -93,14 +93,14 @@ def test_path_params_retrieve(self, client: Browserbase) -> None: @parametrize def test_method_update(self, client: Browserbase) -> None: context = client.contexts.update( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + "id", ) assert_matches_type(ContextUpdateResponse, context, path=["response"]) @parametrize def test_raw_response_update(self, client: Browserbase) -> None: response = client.contexts.with_raw_response.update( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + "id", ) assert response.is_closed is True @@ -111,7 +111,7 @@ def test_raw_response_update(self, client: Browserbase) -> None: @parametrize def test_streaming_response_update(self, client: Browserbase) -> None: with client.contexts.with_streaming_response.update( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + "id", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -137,14 +137,14 @@ class TestAsyncContexts: @parametrize async def test_method_create(self, async_client: AsyncBrowserbase) -> None: context = await async_client.contexts.create( - project_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + project_id="projectId", ) assert_matches_type(ContextCreateResponse, context, path=["response"]) @parametrize async def test_raw_response_create(self, async_client: AsyncBrowserbase) -> None: response = await async_client.contexts.with_raw_response.create( - project_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + project_id="projectId", ) assert response.is_closed is True @@ -155,7 +155,7 @@ async def test_raw_response_create(self, async_client: AsyncBrowserbase) -> None @parametrize async def test_streaming_response_create(self, async_client: AsyncBrowserbase) -> None: async with async_client.contexts.with_streaming_response.create( - project_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + project_id="projectId", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -168,14 +168,14 @@ async def test_streaming_response_create(self, async_client: AsyncBrowserbase) - @parametrize async def test_method_retrieve(self, async_client: AsyncBrowserbase) -> None: context = await async_client.contexts.retrieve( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + "id", ) assert_matches_type(ContextRetrieveResponse, context, path=["response"]) @parametrize async def test_raw_response_retrieve(self, async_client: AsyncBrowserbase) -> None: response = await async_client.contexts.with_raw_response.retrieve( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + "id", ) assert response.is_closed is True @@ -186,7 +186,7 @@ async def test_raw_response_retrieve(self, async_client: AsyncBrowserbase) -> No @parametrize async def test_streaming_response_retrieve(self, async_client: AsyncBrowserbase) -> None: async with async_client.contexts.with_streaming_response.retrieve( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + "id", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -206,14 +206,14 @@ async def test_path_params_retrieve(self, async_client: AsyncBrowserbase) -> Non @parametrize async def test_method_update(self, async_client: AsyncBrowserbase) -> None: context = await async_client.contexts.update( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + "id", ) assert_matches_type(ContextUpdateResponse, context, path=["response"]) @parametrize async def test_raw_response_update(self, async_client: AsyncBrowserbase) -> None: response = await async_client.contexts.with_raw_response.update( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + "id", ) assert response.is_closed is True @@ -224,7 +224,7 @@ async def test_raw_response_update(self, async_client: AsyncBrowserbase) -> None @parametrize async def test_streaming_response_update(self, async_client: AsyncBrowserbase) -> None: async with async_client.contexts.with_streaming_response.update( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + "id", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" diff --git a/tests/api_resources/test_extensions.py b/tests/api_resources/test_extensions.py index 2a6e0ce2..e32ae9b0 100644 --- a/tests/api_resources/test_extensions.py +++ b/tests/api_resources/test_extensions.py @@ -51,14 +51,14 @@ def test_streaming_response_create(self, client: Browserbase) -> None: @parametrize def test_method_retrieve(self, client: Browserbase) -> None: extension = client.extensions.retrieve( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + "id", ) assert_matches_type(ExtensionRetrieveResponse, extension, path=["response"]) @parametrize def test_raw_response_retrieve(self, client: Browserbase) -> None: response = client.extensions.with_raw_response.retrieve( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + "id", ) assert response.is_closed is True @@ -69,7 +69,7 @@ def test_raw_response_retrieve(self, client: Browserbase) -> None: @parametrize def test_streaming_response_retrieve(self, client: Browserbase) -> None: with client.extensions.with_streaming_response.retrieve( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + "id", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -89,14 +89,14 @@ def test_path_params_retrieve(self, client: Browserbase) -> None: @parametrize def test_method_delete(self, client: Browserbase) -> None: extension = client.extensions.delete( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + "id", ) assert extension is None @parametrize def test_raw_response_delete(self, client: Browserbase) -> None: response = client.extensions.with_raw_response.delete( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + "id", ) assert response.is_closed is True @@ -107,7 +107,7 @@ def test_raw_response_delete(self, client: Browserbase) -> None: @parametrize def test_streaming_response_delete(self, client: Browserbase) -> None: with client.extensions.with_streaming_response.delete( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + "id", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -164,14 +164,14 @@ async def test_streaming_response_create(self, async_client: AsyncBrowserbase) - @parametrize async def test_method_retrieve(self, async_client: AsyncBrowserbase) -> None: extension = await async_client.extensions.retrieve( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + "id", ) assert_matches_type(ExtensionRetrieveResponse, extension, path=["response"]) @parametrize async def test_raw_response_retrieve(self, async_client: AsyncBrowserbase) -> None: response = await async_client.extensions.with_raw_response.retrieve( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + "id", ) assert response.is_closed is True @@ -182,7 +182,7 @@ async def test_raw_response_retrieve(self, async_client: AsyncBrowserbase) -> No @parametrize async def test_streaming_response_retrieve(self, async_client: AsyncBrowserbase) -> None: async with async_client.extensions.with_streaming_response.retrieve( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + "id", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -202,14 +202,14 @@ async def test_path_params_retrieve(self, async_client: AsyncBrowserbase) -> Non @parametrize async def test_method_delete(self, async_client: AsyncBrowserbase) -> None: extension = await async_client.extensions.delete( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + "id", ) assert extension is None @parametrize async def test_raw_response_delete(self, async_client: AsyncBrowserbase) -> None: response = await async_client.extensions.with_raw_response.delete( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + "id", ) assert response.is_closed is True @@ -220,7 +220,7 @@ async def test_raw_response_delete(self, async_client: AsyncBrowserbase) -> None @parametrize async def test_streaming_response_delete(self, async_client: AsyncBrowserbase) -> None: async with async_client.extensions.with_streaming_response.delete( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + "id", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" diff --git a/tests/api_resources/test_projects.py b/tests/api_resources/test_projects.py index 5217503f..0d8e3c94 100644 --- a/tests/api_resources/test_projects.py +++ b/tests/api_resources/test_projects.py @@ -20,14 +20,14 @@ class TestProjects: @parametrize def test_method_retrieve(self, client: Browserbase) -> None: project = client.projects.retrieve( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + "id", ) assert_matches_type(ProjectRetrieveResponse, project, path=["response"]) @parametrize def test_raw_response_retrieve(self, client: Browserbase) -> None: response = client.projects.with_raw_response.retrieve( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + "id", ) assert response.is_closed is True @@ -38,7 +38,7 @@ def test_raw_response_retrieve(self, client: Browserbase) -> None: @parametrize def test_streaming_response_retrieve(self, client: Browserbase) -> None: with client.projects.with_streaming_response.retrieve( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + "id", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -83,14 +83,14 @@ def test_streaming_response_list(self, client: Browserbase) -> None: @parametrize def test_method_usage(self, client: Browserbase) -> None: project = client.projects.usage( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + "id", ) assert_matches_type(ProjectUsageResponse, project, path=["response"]) @parametrize def test_raw_response_usage(self, client: Browserbase) -> None: response = client.projects.with_raw_response.usage( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + "id", ) assert response.is_closed is True @@ -101,7 +101,7 @@ def test_raw_response_usage(self, client: Browserbase) -> None: @parametrize def test_streaming_response_usage(self, client: Browserbase) -> None: with client.projects.with_streaming_response.usage( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + "id", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -127,14 +127,14 @@ class TestAsyncProjects: @parametrize async def test_method_retrieve(self, async_client: AsyncBrowserbase) -> None: project = await async_client.projects.retrieve( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + "id", ) assert_matches_type(ProjectRetrieveResponse, project, path=["response"]) @parametrize async def test_raw_response_retrieve(self, async_client: AsyncBrowserbase) -> None: response = await async_client.projects.with_raw_response.retrieve( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + "id", ) assert response.is_closed is True @@ -145,7 +145,7 @@ async def test_raw_response_retrieve(self, async_client: AsyncBrowserbase) -> No @parametrize async def test_streaming_response_retrieve(self, async_client: AsyncBrowserbase) -> None: async with async_client.projects.with_streaming_response.retrieve( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + "id", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -190,14 +190,14 @@ async def test_streaming_response_list(self, async_client: AsyncBrowserbase) -> @parametrize async def test_method_usage(self, async_client: AsyncBrowserbase) -> None: project = await async_client.projects.usage( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + "id", ) assert_matches_type(ProjectUsageResponse, project, path=["response"]) @parametrize async def test_raw_response_usage(self, async_client: AsyncBrowserbase) -> None: response = await async_client.projects.with_raw_response.usage( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + "id", ) assert response.is_closed is True @@ -208,7 +208,7 @@ async def test_raw_response_usage(self, async_client: AsyncBrowserbase) -> None: @parametrize async def test_streaming_response_usage(self, async_client: AsyncBrowserbase) -> None: async with async_client.projects.with_streaming_response.usage( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + "id", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" diff --git a/tests/api_resources/test_sessions.py b/tests/api_resources/test_sessions.py index 11471ecd..1838a8a1 100644 --- a/tests/api_resources/test_sessions.py +++ b/tests/api_resources/test_sessions.py @@ -26,24 +26,24 @@ class TestSessions: @parametrize def test_method_create(self, client: Browserbase) -> None: session = client.sessions.create( - project_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + project_id="projectId", ) assert_matches_type(SessionCreateResponse, session, path=["response"]) @parametrize def test_method_create_with_all_params(self, client: Browserbase) -> None: session = client.sessions.create( - project_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + project_id="projectId", browser_settings={ "advanced_stealth": True, "block_ads": True, "captcha_image_selector": "captchaImageSelector", "captcha_input_selector": "captchaInputSelector", "context": { - "id": "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + "id": "id", "persist": True, }, - "extension_id": "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + "extension_id": "extensionId", "fingerprint": { "browsers": ["chrome"], "devices": ["desktop"], @@ -65,7 +65,7 @@ def test_method_create_with_all_params(self, client: Browserbase) -> None: "width": 0, }, }, - extension_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + extension_id="extensionId", keep_alive=True, proxies=[ { @@ -87,7 +87,7 @@ def test_method_create_with_all_params(self, client: Browserbase) -> None: @parametrize def test_raw_response_create(self, client: Browserbase) -> None: response = client.sessions.with_raw_response.create( - project_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + project_id="projectId", ) assert response.is_closed is True @@ -98,7 +98,7 @@ def test_raw_response_create(self, client: Browserbase) -> None: @parametrize def test_streaming_response_create(self, client: Browserbase) -> None: with client.sessions.with_streaming_response.create( - project_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + project_id="projectId", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -111,14 +111,14 @@ def test_streaming_response_create(self, client: Browserbase) -> None: @parametrize def test_method_retrieve(self, client: Browserbase) -> None: session = client.sessions.retrieve( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + "id", ) assert_matches_type(SessionRetrieveResponse, session, path=["response"]) @parametrize def test_raw_response_retrieve(self, client: Browserbase) -> None: response = client.sessions.with_raw_response.retrieve( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + "id", ) assert response.is_closed is True @@ -129,7 +129,7 @@ def test_raw_response_retrieve(self, client: Browserbase) -> None: @parametrize def test_streaming_response_retrieve(self, client: Browserbase) -> None: with client.sessions.with_streaming_response.retrieve( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + "id", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -149,8 +149,8 @@ def test_path_params_retrieve(self, client: Browserbase) -> None: @parametrize def test_method_update(self, client: Browserbase) -> None: session = client.sessions.update( - id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - project_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + id="id", + project_id="projectId", status="REQUEST_RELEASE", ) assert_matches_type(SessionUpdateResponse, session, path=["response"]) @@ -158,8 +158,8 @@ def test_method_update(self, client: Browserbase) -> None: @parametrize def test_raw_response_update(self, client: Browserbase) -> None: response = client.sessions.with_raw_response.update( - id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - project_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + id="id", + project_id="projectId", status="REQUEST_RELEASE", ) @@ -171,8 +171,8 @@ def test_raw_response_update(self, client: Browserbase) -> None: @parametrize def test_streaming_response_update(self, client: Browserbase) -> None: with client.sessions.with_streaming_response.update( - id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - project_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + id="id", + project_id="projectId", status="REQUEST_RELEASE", ) as response: assert not response.is_closed @@ -188,7 +188,7 @@ def test_path_params_update(self, client: Browserbase) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): client.sessions.with_raw_response.update( id="", - project_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + project_id="projectId", status="REQUEST_RELEASE", ) @@ -228,14 +228,14 @@ def test_streaming_response_list(self, client: Browserbase) -> None: @parametrize def test_method_debug(self, client: Browserbase) -> None: session = client.sessions.debug( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + "id", ) assert_matches_type(SessionDebugResponse, session, path=["response"]) @parametrize def test_raw_response_debug(self, client: Browserbase) -> None: response = client.sessions.with_raw_response.debug( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + "id", ) assert response.is_closed is True @@ -246,7 +246,7 @@ def test_raw_response_debug(self, client: Browserbase) -> None: @parametrize def test_streaming_response_debug(self, client: Browserbase) -> None: with client.sessions.with_streaming_response.debug( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + "id", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -272,24 +272,24 @@ class TestAsyncSessions: @parametrize async def test_method_create(self, async_client: AsyncBrowserbase) -> None: session = await async_client.sessions.create( - project_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + project_id="projectId", ) assert_matches_type(SessionCreateResponse, session, path=["response"]) @parametrize async def test_method_create_with_all_params(self, async_client: AsyncBrowserbase) -> None: session = await async_client.sessions.create( - project_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + project_id="projectId", browser_settings={ "advanced_stealth": True, "block_ads": True, "captcha_image_selector": "captchaImageSelector", "captcha_input_selector": "captchaInputSelector", "context": { - "id": "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + "id": "id", "persist": True, }, - "extension_id": "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + "extension_id": "extensionId", "fingerprint": { "browsers": ["chrome"], "devices": ["desktop"], @@ -311,7 +311,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncBrowserbas "width": 0, }, }, - extension_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + extension_id="extensionId", keep_alive=True, proxies=[ { @@ -333,7 +333,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncBrowserbas @parametrize async def test_raw_response_create(self, async_client: AsyncBrowserbase) -> None: response = await async_client.sessions.with_raw_response.create( - project_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + project_id="projectId", ) assert response.is_closed is True @@ -344,7 +344,7 @@ async def test_raw_response_create(self, async_client: AsyncBrowserbase) -> None @parametrize async def test_streaming_response_create(self, async_client: AsyncBrowserbase) -> None: async with async_client.sessions.with_streaming_response.create( - project_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + project_id="projectId", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -357,14 +357,14 @@ async def test_streaming_response_create(self, async_client: AsyncBrowserbase) - @parametrize async def test_method_retrieve(self, async_client: AsyncBrowserbase) -> None: session = await async_client.sessions.retrieve( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + "id", ) assert_matches_type(SessionRetrieveResponse, session, path=["response"]) @parametrize async def test_raw_response_retrieve(self, async_client: AsyncBrowserbase) -> None: response = await async_client.sessions.with_raw_response.retrieve( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + "id", ) assert response.is_closed is True @@ -375,7 +375,7 @@ async def test_raw_response_retrieve(self, async_client: AsyncBrowserbase) -> No @parametrize async def test_streaming_response_retrieve(self, async_client: AsyncBrowserbase) -> None: async with async_client.sessions.with_streaming_response.retrieve( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + "id", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -395,8 +395,8 @@ async def test_path_params_retrieve(self, async_client: AsyncBrowserbase) -> Non @parametrize async def test_method_update(self, async_client: AsyncBrowserbase) -> None: session = await async_client.sessions.update( - id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - project_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + id="id", + project_id="projectId", status="REQUEST_RELEASE", ) assert_matches_type(SessionUpdateResponse, session, path=["response"]) @@ -404,8 +404,8 @@ async def test_method_update(self, async_client: AsyncBrowserbase) -> None: @parametrize async def test_raw_response_update(self, async_client: AsyncBrowserbase) -> None: response = await async_client.sessions.with_raw_response.update( - id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - project_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + id="id", + project_id="projectId", status="REQUEST_RELEASE", ) @@ -417,8 +417,8 @@ async def test_raw_response_update(self, async_client: AsyncBrowserbase) -> None @parametrize async def test_streaming_response_update(self, async_client: AsyncBrowserbase) -> None: async with async_client.sessions.with_streaming_response.update( - id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - project_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + id="id", + project_id="projectId", status="REQUEST_RELEASE", ) as response: assert not response.is_closed @@ -434,7 +434,7 @@ async def test_path_params_update(self, async_client: AsyncBrowserbase) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): await async_client.sessions.with_raw_response.update( id="", - project_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + project_id="projectId", status="REQUEST_RELEASE", ) @@ -474,14 +474,14 @@ async def test_streaming_response_list(self, async_client: AsyncBrowserbase) -> @parametrize async def test_method_debug(self, async_client: AsyncBrowserbase) -> None: session = await async_client.sessions.debug( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + "id", ) assert_matches_type(SessionDebugResponse, session, path=["response"]) @parametrize async def test_raw_response_debug(self, async_client: AsyncBrowserbase) -> None: response = await async_client.sessions.with_raw_response.debug( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + "id", ) assert response.is_closed is True @@ -492,7 +492,7 @@ async def test_raw_response_debug(self, async_client: AsyncBrowserbase) -> None: @parametrize async def test_streaming_response_debug(self, async_client: AsyncBrowserbase) -> None: async with async_client.sessions.with_streaming_response.debug( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + "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 1e26547d..bf058259 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -727,9 +727,7 @@ def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter, clien respx_mock.post("/v1/sessions").mock(side_effect=httpx.TimeoutException("Test timeout error")) with pytest.raises(APITimeoutError): - client.sessions.with_streaming_response.create( - project_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e" - ).__enter__() + client.sessions.with_streaming_response.create(project_id="projectId").__enter__() assert _get_open_connections(self.client) == 0 @@ -739,9 +737,7 @@ def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter, client respx_mock.post("/v1/sessions").mock(return_value=httpx.Response(500)) with pytest.raises(APIStatusError): - client.sessions.with_streaming_response.create( - project_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e" - ).__enter__() + client.sessions.with_streaming_response.create(project_id="projectId").__enter__() assert _get_open_connections(self.client) == 0 @pytest.mark.parametrize("failures_before_success", [0, 2, 4]) @@ -770,7 +766,7 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: respx_mock.post("/v1/sessions").mock(side_effect=retry_handler) - response = client.sessions.with_raw_response.create(project_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e") + response = client.sessions.with_raw_response.create(project_id="projectId") assert response.retries_taken == failures_before_success assert int(response.http_request.headers.get("x-stainless-retry-count")) == failures_before_success @@ -795,7 +791,7 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: respx_mock.post("/v1/sessions").mock(side_effect=retry_handler) response = client.sessions.with_raw_response.create( - project_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", extra_headers={"x-stainless-retry-count": Omit()} + project_id="projectId", extra_headers={"x-stainless-retry-count": Omit()} ) assert len(response.http_request.headers.get_list("x-stainless-retry-count")) == 0 @@ -820,7 +816,7 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: respx_mock.post("/v1/sessions").mock(side_effect=retry_handler) response = client.sessions.with_raw_response.create( - project_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", extra_headers={"x-stainless-retry-count": "42"} + project_id="projectId", extra_headers={"x-stainless-retry-count": "42"} ) assert response.http_request.headers.get("x-stainless-retry-count") == "42" @@ -1552,9 +1548,7 @@ async def test_retrying_timeout_errors_doesnt_leak( respx_mock.post("/v1/sessions").mock(side_effect=httpx.TimeoutException("Test timeout error")) with pytest.raises(APITimeoutError): - await async_client.sessions.with_streaming_response.create( - project_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e" - ).__aenter__() + await async_client.sessions.with_streaming_response.create(project_id="projectId").__aenter__() assert _get_open_connections(self.client) == 0 @@ -1566,9 +1560,7 @@ async def test_retrying_status_errors_doesnt_leak( respx_mock.post("/v1/sessions").mock(return_value=httpx.Response(500)) with pytest.raises(APIStatusError): - await async_client.sessions.with_streaming_response.create( - project_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e" - ).__aenter__() + await async_client.sessions.with_streaming_response.create(project_id="projectId").__aenter__() assert _get_open_connections(self.client) == 0 @pytest.mark.parametrize("failures_before_success", [0, 2, 4]) @@ -1598,7 +1590,7 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: respx_mock.post("/v1/sessions").mock(side_effect=retry_handler) - response = await client.sessions.with_raw_response.create(project_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e") + response = await client.sessions.with_raw_response.create(project_id="projectId") assert response.retries_taken == failures_before_success assert int(response.http_request.headers.get("x-stainless-retry-count")) == failures_before_success @@ -1624,7 +1616,7 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: respx_mock.post("/v1/sessions").mock(side_effect=retry_handler) response = await client.sessions.with_raw_response.create( - project_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", extra_headers={"x-stainless-retry-count": Omit()} + project_id="projectId", extra_headers={"x-stainless-retry-count": Omit()} ) assert len(response.http_request.headers.get_list("x-stainless-retry-count")) == 0 @@ -1650,7 +1642,7 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: respx_mock.post("/v1/sessions").mock(side_effect=retry_handler) response = await client.sessions.with_raw_response.create( - project_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", extra_headers={"x-stainless-retry-count": "42"} + project_id="projectId", extra_headers={"x-stainless-retry-count": "42"} ) assert response.http_request.headers.get("x-stainless-retry-count") == "42" From 7be44b39f10378dac2f1cb92d6fb583ed70fc279 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 22 Jul 2025 02:17:23 +0000 Subject: [PATCH 177/330] fix(parsing): ignore empty metadata --- src/browserbase/_models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/browserbase/_models.py b/src/browserbase/_models.py index 528d5680..ffcbf67b 100644 --- a/src/browserbase/_models.py +++ b/src/browserbase/_models.py @@ -439,7 +439,7 @@ def construct_type(*, value: object, type_: object, metadata: Optional[List[Any] type_ = type_.__value__ # type: ignore[unreachable] # unwrap `Annotated[T, ...]` -> `T` - if metadata is not None: + if metadata is not None and len(metadata) > 0: meta: tuple[Any, ...] = tuple(metadata) elif is_annotated_type(type_): meta = get_args(type_)[1:] From 3bb754af115a4b8966daa0f732eb1b992d11ed25 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 23 Jul 2025 02:20:59 +0000 Subject: [PATCH 178/330] fix(parsing): parse extra field types --- src/browserbase/_models.py | 25 +++++++++++++++++++++++-- tests/test_models.py | 29 ++++++++++++++++++++++++++++- 2 files changed, 51 insertions(+), 3 deletions(-) diff --git a/src/browserbase/_models.py b/src/browserbase/_models.py index ffcbf67b..b8387ce9 100644 --- a/src/browserbase/_models.py +++ b/src/browserbase/_models.py @@ -208,14 +208,18 @@ def construct( # pyright: ignore[reportIncompatibleMethodOverride] 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_V2: - _extra[key] = value + _extra[key] = parsed else: _fields_set.add(key) - fields_values[key] = value + fields_values[key] = parsed object.__setattr__(m, "__dict__", fields_values) @@ -370,6 +374,23 @@ def _construct_field(value: object, field: FieldInfo, key: str) -> object: return construct_type(value=value, type_=type_, metadata=getattr(field, "metadata", None)) +def _get_extra_fields_type(cls: type[pydantic.BaseModel]) -> type | None: + if not PYDANTIC_V2: + # 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_): diff --git a/tests/test_models.py b/tests/test_models.py index 51fabb73..49ba4b5b 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -1,5 +1,5 @@ import json -from typing import Any, Dict, List, Union, Optional, cast +from typing import TYPE_CHECKING, Any, Dict, List, Union, Optional, cast from datetime import datetime, timezone from typing_extensions import Literal, Annotated, TypeAliasType @@ -934,3 +934,30 @@ class Type2(BaseModel): ) assert isinstance(model, Type1) assert isinstance(model.value, InnerType2) + + +@pytest.mark.skipif(not PYDANTIC_V2, 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" From 9c232317cd8dfa92fed80d3a7e4ec77aab5a4c45 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 25 Jul 2025 04:46:50 +0000 Subject: [PATCH 179/330] chore(project): add settings file for vscode --- .gitignore | 1 - .vscode/settings.json | 3 +++ 2 files changed, 3 insertions(+), 1 deletion(-) create mode 100644 .vscode/settings.json diff --git a/.gitignore b/.gitignore index 87797408..95ceb189 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,4 @@ .prism.log -.vscode _dev __pycache__ 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", +} From 8d0b7f012da203bd30fff2f199c3b3188087ae2b Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 31 Jul 2025 06:42:30 +0000 Subject: [PATCH 180/330] feat(client): support file upload requests --- src/browserbase/_base_client.py | 5 ++++- src/browserbase/_files.py | 8 ++++---- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/browserbase/_base_client.py b/src/browserbase/_base_client.py index 434b1e8c..f2131454 100644 --- a/src/browserbase/_base_client.py +++ b/src/browserbase/_base_client.py @@ -532,7 +532,10 @@ def _build_request( is_body_allowed = options.method.lower() != "get" if is_body_allowed: - kwargs["json"] = json_data if is_given(json_data) else None + 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) diff --git a/src/browserbase/_files.py b/src/browserbase/_files.py index c690226c..ff951be7 100644 --- a/src/browserbase/_files.py +++ b/src/browserbase/_files.py @@ -69,12 +69,12 @@ def _transform_file(file: FileTypes) -> HttpxFileTypes: return file if is_tuple_t(file): - return (file[0], _read_file_content(file[1]), *file[2:]) + 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: +def read_file_content(file: FileContent) -> HttpxFileContent: if isinstance(file, os.PathLike): return pathlib.Path(file).read_bytes() return file @@ -111,12 +111,12 @@ async def _async_transform_file(file: FileTypes) -> HttpxFileTypes: return file if is_tuple_t(file): - return (file[0], await _async_read_file_content(file[1]), *file[2:]) + 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: +async def async_read_file_content(file: FileContent) -> HttpxFileContent: if isinstance(file, os.PathLike): return await anyio.Path(file).read_bytes() From 1147639e35e1778e65cad67219e2a755c7d8f873 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 6 Aug 2025 08:09:32 +0000 Subject: [PATCH 181/330] chore(internal): fix ruff target version --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 84bbf0a2..95b7e9c1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -159,7 +159,7 @@ reportPrivateUsage = false [tool.ruff] line-length = 120 output-format = "grouped" -target-version = "py37" +target-version = "py38" [tool.ruff.format] docstring-code-format = true From 4c9adf9cf21c2941e2c8afda1aeb3e2acc6e34d1 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 9 Aug 2025 05:04:37 +0000 Subject: [PATCH 182/330] chore: update @stainless-api/prism-cli to v5.15.0 --- scripts/mock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/mock b/scripts/mock index d2814ae6..0b28f6ea 100755 --- a/scripts/mock +++ b/scripts/mock @@ -21,7 +21,7 @@ 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.8.5 -- prism mock "$URL" &> .prism.log & + 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" @@ -37,5 +37,5 @@ if [ "$1" == "--daemon" ]; then echo else - npm exec --package=@stainless-api/prism-cli@5.8.5 -- prism mock "$URL" + npm exec --package=@stainless-api/prism-cli@5.15.0 -- prism mock "$URL" fi From f627e161d5a17a6488ab57c15e475a3749c81448 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 9 Aug 2025 05:20:18 +0000 Subject: [PATCH 183/330] chore(internal): update comment in script --- scripts/test | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/test b/scripts/test index 2b878456..dbeda2d2 100755 --- a/scripts/test +++ b/scripts/test @@ -43,7 +43,7 @@ elif ! prism_is_running ; then 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=@stoplight/prism-cli@~5.3.2 -- prism mock path/to/your.openapi.yml${NC}" + echo -e " \$ ${YELLOW}npm exec --package=@stainless-api/prism-cli@5.15.0 -- prism mock path/to/your.openapi.yml${NC}" echo exit 1 From a0182527755f40e50c0d8909e4d42776d333f3f4 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 22 Aug 2025 07:16:48 +0000 Subject: [PATCH 184/330] chore: update github action --- .github/workflows/ci.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 455b6dc7..9fdfbede 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -36,7 +36,7 @@ jobs: run: ./scripts/lint build: - if: github.repository == 'stainless-sdks/browserbase-python' && (github.event_name == 'push' || github.event.pull_request.head.repo.fork) + if: github.event_name == 'push' || github.event.pull_request.head.repo.fork timeout-minutes: 10 name: build permissions: @@ -61,12 +61,14 @@ jobs: run: rye build - name: Get GitHub OIDC Token + if: github.repository == 'stainless-sdks/browserbase-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/browserbase-python' env: URL: https://pkg.stainless.com/s AUTH: ${{ steps.github-oidc.outputs.github_token }} From 14fa5556967df34aa816c32ade515149a5f4ea07 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 26 Aug 2025 05:47:40 +0000 Subject: [PATCH 185/330] chore(internal): change ci workflow machines --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9fdfbede..8edf5a60 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -42,7 +42,7 @@ jobs: permissions: contents: read id-token: write - runs-on: depot-ubuntu-24.04 + runs-on: ${{ github.repository == 'stainless-sdks/browserbase-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} steps: - uses: actions/checkout@v4 From 613dd358a4f1a96cce2093a7613ad9969684d8c9 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 27 Aug 2025 02:10:45 +0000 Subject: [PATCH 186/330] codegen metadata --- .stats.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index 772d8de0..fc1eb92e 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 18 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fbrowserbase-3d350e6cd04452a1654fdb7a93fa7e8dbbf7706273ae7c21818efce9dcf9bbfe.yml -openapi_spec_hash: 25beffd2761e5414d0cb32f74a969a38 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fbrowserbase-c14a7d6b23a7fd42a26a7c55a668d1dcd2e4b58354b878e696bc959d808c71c9.yml +openapi_spec_hash: a0878bab95e435f9ce0d2418f0784d06 config_hash: b3ca4ec5b02e5333af51ebc2e9fdef1b From dd8817c093b7a451ab354d9a49379f3bd657fb57 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 27 Aug 2025 08:16:15 +0000 Subject: [PATCH 187/330] fix: avoid newer type syntax --- src/browserbase/_models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/browserbase/_models.py b/src/browserbase/_models.py index b8387ce9..92f7c10b 100644 --- a/src/browserbase/_models.py +++ b/src/browserbase/_models.py @@ -304,7 +304,7 @@ def model_dump( exclude_none=exclude_none, ) - return cast(dict[str, Any], json_safe(dumped)) if mode == "json" else dumped + return cast("dict[str, Any]", json_safe(dumped)) if mode == "json" else dumped @override def model_dump_json( From 9b8b9fbd7fb344b9b78051707e35fa2f01f4970c Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 27 Aug 2025 08:20:06 +0000 Subject: [PATCH 188/330] chore(internal): update pyright exclude list --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 95b7e9c1..40f7993a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -148,6 +148,7 @@ exclude = [ "_dev", ".venv", ".nox", + ".git", ] reportImplicitOverride = true From ba3465857652385bebcf4fac6801b1bc79f6f06d Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 30 Aug 2025 04:17:00 +0000 Subject: [PATCH 189/330] chore(internal): add Sequence related utils --- src/browserbase/_types.py | 36 +++++++++++++++++++++++++++++- src/browserbase/_utils/__init__.py | 1 + src/browserbase/_utils/_typing.py | 5 +++++ tests/utils.py | 10 ++++++++- 4 files changed, 50 insertions(+), 2 deletions(-) diff --git a/src/browserbase/_types.py b/src/browserbase/_types.py index b07c0e14..b954306a 100644 --- a/src/browserbase/_types.py +++ b/src/browserbase/_types.py @@ -13,10 +13,21 @@ Mapping, TypeVar, Callable, + Iterator, Optional, Sequence, ) -from typing_extensions import Set, Literal, Protocol, TypeAlias, TypedDict, override, runtime_checkable +from typing_extensions import ( + Set, + Literal, + Protocol, + TypeAlias, + TypedDict, + SupportsIndex, + overload, + override, + runtime_checkable, +) import httpx import pydantic @@ -217,3 +228,26 @@ class _GenericAlias(Protocol): 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 + 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 index(self, value: Any, start: int = 0, stop: int = ..., /) -> int: ... + def count(self, value: Any, /) -> int: ... + 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/browserbase/_utils/__init__.py b/src/browserbase/_utils/__init__.py index d4fda26f..ca547ce5 100644 --- a/src/browserbase/_utils/__init__.py +++ b/src/browserbase/_utils/__init__.py @@ -38,6 +38,7 @@ 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, diff --git a/src/browserbase/_utils/_typing.py b/src/browserbase/_utils/_typing.py index 1bac9542..845cd6b2 100644 --- a/src/browserbase/_utils/_typing.py +++ b/src/browserbase/_utils/_typing.py @@ -26,6 +26,11 @@ 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 diff --git a/tests/utils.py b/tests/utils.py index ac183a7e..56d3d8c1 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -4,7 +4,7 @@ import inspect import traceback import contextlib -from typing import Any, TypeVar, Iterator, cast +from typing import Any, TypeVar, Iterator, Sequence, cast from datetime import date, datetime from typing_extensions import Literal, get_args, get_origin, assert_type @@ -15,6 +15,7 @@ is_list_type, is_union_type, extract_type_arg, + is_sequence_type, is_annotated_type, is_type_alias_type, ) @@ -71,6 +72,13 @@ def assert_matches_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: From bec04e23d09e1679a53e639d27c561a6a7b02b30 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 3 Sep 2025 03:44:49 +0000 Subject: [PATCH 190/330] feat(types): replace List[str] with SequenceNotStr in params --- src/browserbase/_utils/_transform.py | 6 ++++++ src/browserbase/types/session_create_params.py | 3 ++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/browserbase/_utils/_transform.py b/src/browserbase/_utils/_transform.py index b0cc20a7..f0bcefd4 100644 --- a/src/browserbase/_utils/_transform.py +++ b/src/browserbase/_utils/_transform.py @@ -16,6 +16,7 @@ lru_cache, is_mapping, is_iterable, + is_sequence, ) from .._files import is_base64_file_input from ._typing import ( @@ -24,6 +25,7 @@ extract_type_arg, is_iterable_type, is_required_type, + is_sequence_type, is_annotated_type, strip_annotated_type, ) @@ -184,6 +186,8 @@ def _transform_recursive( (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. @@ -346,6 +350,8 @@ async def _async_transform_recursive( (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. diff --git a/src/browserbase/types/session_create_params.py b/src/browserbase/types/session_create_params.py index f3c49179..3c96dd03 100644 --- a/src/browserbase/types/session_create_params.py +++ b/src/browserbase/types/session_create_params.py @@ -5,6 +5,7 @@ from typing import Dict, List, Union, Iterable from typing_extensions import Literal, Required, Annotated, TypeAlias, TypedDict +from .._types import SequenceNotStr from .._utils import PropertyInfo __all__ = [ @@ -90,7 +91,7 @@ class BrowserSettingsFingerprint(TypedDict, total=False): http_version: Annotated[Literal["1", "2"], PropertyInfo(alias="httpVersion")] - locales: List[str] + locales: SequenceNotStr[str] operating_systems: Annotated[ List[Literal["android", "ios", "linux", "macos", "windows"]], PropertyInfo(alias="operatingSystems") From 3cbbc739dffece61819b4b2c868db8f3fe41c130 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 4 Sep 2025 03:49:38 +0000 Subject: [PATCH 191/330] feat: improve future compat with pydantic v3 --- src/browserbase/_base_client.py | 6 +- src/browserbase/_compat.py | 96 +++++++-------- src/browserbase/_models.py | 80 ++++++------- src/browserbase/_utils/__init__.py | 10 +- src/browserbase/_utils/_compat.py | 45 +++++++ src/browserbase/_utils/_datetime_parse.py | 136 ++++++++++++++++++++++ src/browserbase/_utils/_transform.py | 6 +- src/browserbase/_utils/_typing.py | 2 +- src/browserbase/_utils/_utils.py | 1 - tests/test_models.py | 48 ++++---- tests/test_transform.py | 16 +-- tests/test_utils/test_datetime_parse.py | 110 +++++++++++++++++ tests/utils.py | 8 +- 13 files changed, 432 insertions(+), 132 deletions(-) create mode 100644 src/browserbase/_utils/_compat.py create mode 100644 src/browserbase/_utils/_datetime_parse.py create mode 100644 tests/test_utils/test_datetime_parse.py diff --git a/src/browserbase/_base_client.py b/src/browserbase/_base_client.py index f2131454..89549337 100644 --- a/src/browserbase/_base_client.py +++ b/src/browserbase/_base_client.py @@ -59,7 +59,7 @@ ModelBuilderProtocol, ) from ._utils import is_dict, is_list, asyncify, is_given, lru_cache, is_mapping -from ._compat import PYDANTIC_V2, model_copy, model_dump +from ._compat import PYDANTIC_V1, model_copy, model_dump from ._models import GenericModel, FinalRequestOptions, validate_type, construct_type from ._response import ( APIResponse, @@ -232,7 +232,7 @@ def _set_private_attributes( model: Type[_T], options: FinalRequestOptions, ) -> None: - if PYDANTIC_V2 and getattr(self, "__pydantic_private__", None) is None: + if (not PYDANTIC_V1) and getattr(self, "__pydantic_private__", None) is None: self.__pydantic_private__ = {} self._model = model @@ -320,7 +320,7 @@ def _set_private_attributes( client: AsyncAPIClient, options: FinalRequestOptions, ) -> None: - if PYDANTIC_V2 and getattr(self, "__pydantic_private__", None) is None: + if (not PYDANTIC_V1) and getattr(self, "__pydantic_private__", None) is None: self.__pydantic_private__ = {} self._model = model diff --git a/src/browserbase/_compat.py b/src/browserbase/_compat.py index 92d9ee61..bdef67f0 100644 --- a/src/browserbase/_compat.py +++ b/src/browserbase/_compat.py @@ -12,14 +12,13 @@ _T = TypeVar("_T") _ModelT = TypeVar("_ModelT", bound=pydantic.BaseModel) -# --------------- Pydantic v2 compatibility --------------- +# --------------- Pydantic v2, v3 compatibility --------------- # Pyright incorrectly reports some of our functions as overriding a method when they don't # pyright: reportIncompatibleMethodOverride=false -PYDANTIC_V2 = pydantic.VERSION.startswith("2.") +PYDANTIC_V1 = pydantic.VERSION.startswith("1.") -# v1 re-exports if TYPE_CHECKING: def parse_date(value: date | StrBytesIntFloat) -> date: # noqa: ARG001 @@ -44,90 +43,92 @@ def is_typeddict(type_: type[Any]) -> bool: # noqa: ARG001 ... else: - if PYDANTIC_V2: - from pydantic.v1.typing import ( + # 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.v1.datetime_parse import parse_date as parse_date, parse_datetime as parse_datetime + from pydantic.datetime_parse import parse_date as parse_date, parse_datetime as parse_datetime else: - from pydantic.typing import ( + 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, ) - from pydantic.datetime_parse import parse_date as parse_date, parse_datetime as parse_datetime # refactored config if TYPE_CHECKING: from pydantic import ConfigDict as ConfigDict else: - if PYDANTIC_V2: - from pydantic import 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_V2: - return model.model_validate(value) - else: + 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_V2: - return field.is_required() - return field.required # type: ignore + 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_V2: - from pydantic_core import PydanticUndefined - - if value == PydanticUndefined: - return None + 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_V2: - return field.annotation - return field.outer_type_ # type: ignore + if PYDANTIC_V1: + return field.outer_type_ # type: ignore + return field.annotation def get_model_config(model: type[pydantic.BaseModel]) -> Any: - if PYDANTIC_V2: - return model.model_config - return model.__config__ # type: ignore + 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_V2: - return model.model_fields - return model.__fields__ # type: ignore + if PYDANTIC_V1: + return model.__fields__ # type: ignore + return model.model_fields def model_copy(model: _ModelT, *, deep: bool = False) -> _ModelT: - if PYDANTIC_V2: - return model.model_copy(deep=deep) - return model.copy(deep=deep) # type: ignore + 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_V2: - return model.model_dump_json(indent=indent) - return model.json(indent=indent) # type: ignore + if PYDANTIC_V1: + return model.json(indent=indent) # type: ignore + return model.model_dump_json(indent=indent) def model_dump( @@ -139,14 +140,14 @@ def model_dump( warnings: bool = True, mode: Literal["json", "python"] = "python", ) -> dict[str, Any]: - if PYDANTIC_V2 or hasattr(model, "model_dump"): + 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=warnings if PYDANTIC_V2 else True, + warnings=True if PYDANTIC_V1 else warnings, ) return cast( "dict[str, Any]", @@ -159,9 +160,9 @@ def model_dump( def model_parse(model: type[_ModelT], data: Any) -> _ModelT: - if PYDANTIC_V2: - return model.model_validate(data) - return model.parse_obj(data) # pyright: ignore[reportDeprecated] + if PYDANTIC_V1: + return model.parse_obj(data) # pyright: ignore[reportDeprecated] + return model.model_validate(data) # generic models @@ -170,17 +171,16 @@ def model_parse(model: type[_ModelT], data: Any) -> _ModelT: class GenericModel(pydantic.BaseModel): ... else: - if PYDANTIC_V2: + 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): ... - else: - import pydantic.generics - - class GenericModel(pydantic.generics.GenericModel, pydantic.BaseModel): ... - # cached properties if TYPE_CHECKING: diff --git a/src/browserbase/_models.py b/src/browserbase/_models.py index 92f7c10b..3a6017ef 100644 --- a/src/browserbase/_models.py +++ b/src/browserbase/_models.py @@ -50,7 +50,7 @@ strip_annotated_type, ) from ._compat import ( - PYDANTIC_V2, + PYDANTIC_V1, ConfigDict, GenericModel as BaseGenericModel, get_args, @@ -81,11 +81,7 @@ class _ConfigProtocol(Protocol): class BaseModel(pydantic.BaseModel): - if PYDANTIC_V2: - model_config: ClassVar[ConfigDict] = ConfigDict( - extra="allow", defer_build=coerce_boolean(os.environ.get("DEFER_PYDANTIC_BUILD", "true")) - ) - else: + if PYDANTIC_V1: @property @override @@ -95,6 +91,10 @@ def model_fields_set(self) -> set[str]: 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, @@ -215,25 +215,25 @@ def construct( # pyright: ignore[reportIncompatibleMethodOverride] 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_V2: - _extra[key] = parsed - else: + if PYDANTIC_V1: _fields_set.add(key) fields_values[key] = parsed + else: + _extra[key] = parsed object.__setattr__(m, "__dict__", fields_values) - if PYDANTIC_V2: - # 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) - else: + 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 @@ -243,7 +243,7 @@ def construct( # pyright: ignore[reportIncompatibleMethodOverride] # although not in practice model_construct = construct - if not PYDANTIC_V2: + 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 @@ -363,10 +363,10 @@ def _construct_field(value: object, field: FieldInfo, key: str) -> object: if value is None: return field_get_default(field) - if PYDANTIC_V2: - type_ = field.annotation - else: + 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}") @@ -375,7 +375,7 @@ def _construct_field(value: object, field: FieldInfo, key: str) -> object: def _get_extra_fields_type(cls: type[pydantic.BaseModel]) -> type | None: - if not PYDANTIC_V2: + if PYDANTIC_V1: # TODO return None @@ -628,30 +628,30 @@ def _build_discriminated_union_meta(*, union: type, meta_annotations: tuple[Any, for variant in get_args(union): variant = strip_annotated_type(variant) if is_basemodel_type(variant): - if PYDANTIC_V2: - field = _extract_field_schema_pv2(variant, discriminator_field_name) - if not field: + 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.get("serialization_alias") - - field_schema = field["schema"] + discriminator_alias = field_info.alias - if field_schema["type"] == "literal": - for entry in cast("LiteralSchema", field_schema)["expected"]: + 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_info = cast("dict[str, FieldInfo]", variant.__fields__).get(discriminator_field_name) # pyright: ignore[reportDeprecated, reportUnnecessaryCast] - if not field_info: + 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_info.alias + discriminator_alias = field.get("serialization_alias") - if (annotation := getattr(field_info, "annotation", None)) and is_literal_type(annotation): - for entry in get_args(annotation): + field_schema = field["schema"] + + if field_schema["type"] == "literal": + for entry in cast("LiteralSchema", field_schema)["expected"]: if isinstance(entry, str): mapping[entry] = variant @@ -714,7 +714,7 @@ class GenericModel(BaseGenericModel, BaseModel): pass -if PYDANTIC_V2: +if not PYDANTIC_V1: from pydantic import TypeAdapter as _TypeAdapter _CachedTypeAdapter = cast("TypeAdapter[object]", lru_cache(maxsize=None)(_TypeAdapter)) @@ -782,12 +782,12 @@ class FinalRequestOptions(pydantic.BaseModel): json_data: Union[Body, None] = None extra_json: Union[AnyMapping, None] = None - if PYDANTIC_V2: - model_config: ClassVar[ConfigDict] = ConfigDict(arbitrary_types_allowed=True) - else: + 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): @@ -820,9 +820,9 @@ def construct( # type: ignore key: strip_not_given(value) for key, value in values.items() } - if PYDANTIC_V2: - return super().model_construct(_fields_set, **kwargs) - return cast(FinalRequestOptions, super().construct(_fields_set, **kwargs)) # pyright: ignore[reportDeprecated] + 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 diff --git a/src/browserbase/_utils/__init__.py b/src/browserbase/_utils/__init__.py index ca547ce5..dc64e29a 100644 --- a/src/browserbase/_utils/__init__.py +++ b/src/browserbase/_utils/__init__.py @@ -10,7 +10,6 @@ lru_cache as lru_cache, is_mapping as is_mapping, is_tuple_t as is_tuple_t, - parse_date as parse_date, is_iterable as is_iterable, is_sequence as is_sequence, coerce_float as coerce_float, @@ -23,7 +22,6 @@ coerce_boolean as coerce_boolean, coerce_integer as coerce_integer, file_from_path as file_from_path, - parse_datetime as parse_datetime, strip_not_given as strip_not_given, deepcopy_minimal as deepcopy_minimal, get_async_library as get_async_library, @@ -32,6 +30,13 @@ 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, @@ -56,3 +61,4 @@ 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/browserbase/_utils/_compat.py b/src/browserbase/_utils/_compat.py new file mode 100644 index 00000000..dd703233 --- /dev/null +++ b/src/browserbase/_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/browserbase/_utils/_datetime_parse.py b/src/browserbase/_utils/_datetime_parse.py new file mode 100644 index 00000000..7cb9d9e6 --- /dev/null +++ b/src/browserbase/_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/browserbase/_utils/_transform.py b/src/browserbase/_utils/_transform.py index f0bcefd4..c19124f0 100644 --- a/src/browserbase/_utils/_transform.py +++ b/src/browserbase/_utils/_transform.py @@ -19,6 +19,7 @@ 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, @@ -29,7 +30,6 @@ is_annotated_type, strip_annotated_type, ) -from .._compat import get_origin, model_dump, is_typeddict _T = TypeVar("_T") @@ -169,6 +169,8 @@ def _transform_recursive( Defaults to the same value as the `annotation` argument. """ + from .._compat import model_dump + if inner_type is None: inner_type = annotation @@ -333,6 +335,8 @@ async def _async_transform_recursive( Defaults to the same value as the `annotation` argument. """ + from .._compat import model_dump + if inner_type is None: inner_type = annotation diff --git a/src/browserbase/_utils/_typing.py b/src/browserbase/_utils/_typing.py index 845cd6b2..193109f3 100644 --- a/src/browserbase/_utils/_typing.py +++ b/src/browserbase/_utils/_typing.py @@ -15,7 +15,7 @@ from ._utils import lru_cache from .._types import InheritsGeneric -from .._compat import is_union as _is_union +from ._compat import is_union as _is_union def is_annotated_type(typ: type) -> bool: diff --git a/src/browserbase/_utils/_utils.py b/src/browserbase/_utils/_utils.py index ea3cf3f2..f0818595 100644 --- a/src/browserbase/_utils/_utils.py +++ b/src/browserbase/_utils/_utils.py @@ -22,7 +22,6 @@ import sniffio from .._types import NotGiven, FileTypes, NotGivenOr, HeadersLike -from .._compat import parse_date as parse_date, parse_datetime as parse_datetime _T = TypeVar("_T") _TupleT = TypeVar("_TupleT", bound=Tuple[object, ...]) diff --git a/tests/test_models.py b/tests/test_models.py index 49ba4b5b..34f87334 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -8,7 +8,7 @@ from pydantic import Field from browserbase._utils import PropertyInfo -from browserbase._compat import PYDANTIC_V2, parse_obj, model_dump, model_json +from browserbase._compat import PYDANTIC_V1, parse_obj, model_dump, model_json from browserbase._models import BaseModel, construct_type @@ -294,12 +294,12 @@ class Model(BaseModel): assert cast(bool, m.foo) is True m = Model.construct(foo={"name": 3}) - if PYDANTIC_V2: - assert isinstance(m.foo, Submodel1) - assert m.foo.name == 3 # type: ignore - else: + 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: @@ -426,10 +426,10 @@ class Model(BaseModel): expected = datetime(2019, 12, 27, 18, 11, 19, 117000, tzinfo=timezone.utc) - if PYDANTIC_V2: - expected_json = '{"created_at":"2019-12-27T18:11:19.117000Z"}' - else: + 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 @@ -531,7 +531,7 @@ class Model2(BaseModel): assert m4.to_dict(mode="python") == {"created_at": datetime.fromisoformat(time_str)} assert m4.to_dict(mode="json") == {"created_at": time_str} - if not PYDANTIC_V2: + if PYDANTIC_V1: with pytest.raises(ValueError, match="warnings is only supported in Pydantic v2"): m.to_dict(warnings=False) @@ -556,7 +556,7 @@ class Model(BaseModel): assert m3.model_dump() == {"foo": None} assert m3.model_dump(exclude_none=True) == {} - if not PYDANTIC_V2: + if PYDANTIC_V1: with pytest.raises(ValueError, match="round_trip is only supported in Pydantic v2"): m.model_dump(round_trip=True) @@ -580,10 +580,10 @@ class Model(BaseModel): assert json.loads(m.to_json()) == {"FOO": "hello"} assert json.loads(m.to_json(use_api_names=False)) == {"foo": "hello"} - if PYDANTIC_V2: - assert m.to_json(indent=None) == '{"FOO":"hello"}' - else: + 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()) == {} @@ -595,7 +595,7 @@ class Model(BaseModel): assert json.loads(m3.to_json()) == {"FOO": None} assert json.loads(m3.to_json(exclude_none=True)) == {} - if not PYDANTIC_V2: + if PYDANTIC_V1: with pytest.raises(ValueError, match="warnings is only supported in Pydantic v2"): m.to_json(warnings=False) @@ -622,7 +622,7 @@ class Model(BaseModel): assert json.loads(m3.model_dump_json()) == {"foo": None} assert json.loads(m3.model_dump_json(exclude_none=True)) == {} - if not PYDANTIC_V2: + if PYDANTIC_V1: with pytest.raises(ValueError, match="round_trip is only supported in Pydantic v2"): m.model_dump_json(round_trip=True) @@ -679,12 +679,12 @@ class B(BaseModel): ) assert isinstance(m, A) assert m.type == "a" - if PYDANTIC_V2: - assert m.data == 100 # type: ignore[comparison-overlap] - else: + 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: @@ -768,12 +768,12 @@ class B(BaseModel): ) assert isinstance(m, A) assert m.foo_type == "a" - if PYDANTIC_V2: - assert m.data == 100 # type: ignore[comparison-overlap] - else: + 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: @@ -833,7 +833,7 @@ class B(BaseModel): assert UnionType.__discriminator__ is discriminator -@pytest.mark.skipif(not PYDANTIC_V2, reason="TypeAliasType is not supported in Pydantic v1") +@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 @@ -849,7 +849,7 @@ class Model(BaseModel): assert m.union == "bar" -@pytest.mark.skipif(not PYDANTIC_V2, reason="TypeAliasType is not supported in Pydantic v1") +@pytest.mark.skipif(PYDANTIC_V1, reason="TypeAliasType is not supported in Pydantic v1") def test_field_named_cls() -> None: class Model(BaseModel): cls: str @@ -936,7 +936,7 @@ class Type2(BaseModel): assert isinstance(model.value, InnerType2) -@pytest.mark.skipif(not PYDANTIC_V2, reason="this is only supported in pydantic v2 for now") +@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 diff --git a/tests/test_transform.py b/tests/test_transform.py index cba80b21..498d0d93 100644 --- a/tests/test_transform.py +++ b/tests/test_transform.py @@ -15,7 +15,7 @@ parse_datetime, async_transform as _async_transform, ) -from browserbase._compat import PYDANTIC_V2 +from browserbase._compat import PYDANTIC_V1 from browserbase._models import BaseModel _T = TypeVar("_T") @@ -189,7 +189,7 @@ class DateModel(BaseModel): @pytest.mark.asyncio async def test_iso8601_format(use_async: bool) -> None: dt = datetime.fromisoformat("2023-02-23T14:16:36.337692+00:00") - tz = "Z" if PYDANTIC_V2 else "+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] @@ -297,11 +297,11 @@ async def test_pydantic_unknown_field(use_async: bool) -> None: @pytest.mark.asyncio async def test_pydantic_mismatched_types(use_async: bool) -> None: model = MyModel.construct(foo=True) - if PYDANTIC_V2: + if PYDANTIC_V1: + params = await transform(model, Any, use_async) + else: with pytest.warns(UserWarning): params = await transform(model, Any, use_async) - else: - params = await transform(model, Any, use_async) assert cast(Any, params) == {"foo": True} @@ -309,11 +309,11 @@ async def test_pydantic_mismatched_types(use_async: bool) -> None: @pytest.mark.asyncio async def test_pydantic_mismatched_object_type(use_async: bool) -> None: model = MyModel.construct(foo=MyModel.construct(hello="world")) - if PYDANTIC_V2: + if PYDANTIC_V1: + params = await transform(model, Any, use_async) + else: with pytest.warns(UserWarning): params = await transform(model, Any, use_async) - else: - params = await transform(model, Any, use_async) assert cast(Any, params) == {"foo": {"hello": "world"}} diff --git a/tests/test_utils/test_datetime_parse.py b/tests/test_utils/test_datetime_parse.py new file mode 100644 index 00000000..2834c471 --- /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 browserbase._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/utils.py b/tests/utils.py index 56d3d8c1..55521a9b 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -19,7 +19,7 @@ is_annotated_type, is_type_alias_type, ) -from browserbase._compat import PYDANTIC_V2, field_outer_type, get_model_fields +from browserbase._compat import PYDANTIC_V1, field_outer_type, get_model_fields from browserbase._models import BaseModel BaseModelT = TypeVar("BaseModelT", bound=BaseModel) @@ -28,12 +28,12 @@ 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_V2: - allow_none = False - else: + 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), From eb07959a9082fc5916c8a9ec9bae02c603bbb006 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 4 Sep 2025 08:52:55 +0000 Subject: [PATCH 192/330] feat(api): manual updates --- .stats.yml | 8 +- api.md | 2 + src/browserbase/resources/contexts.py | 82 +++++++++++++++++- .../resources/sessions/downloads.py | 86 ++++++++++++++++++- .../types/session_create_params.py | 6 ++ .../api_resources/sessions/test_downloads.py | 76 ++++++++++++++++ tests/api_resources/test_contexts.py | 76 ++++++++++++++++ tests/api_resources/test_sessions.py | 2 + 8 files changed, 332 insertions(+), 6 deletions(-) diff --git a/.stats.yml b/.stats.yml index fc1eb92e..05e31b2a 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ -configured_endpoints: 18 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fbrowserbase-c14a7d6b23a7fd42a26a7c55a668d1dcd2e4b58354b878e696bc959d808c71c9.yml -openapi_spec_hash: a0878bab95e435f9ce0d2418f0784d06 -config_hash: b3ca4ec5b02e5333af51ebc2e9fdef1b +configured_endpoints: 20 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fbrowserbase-be7a4aeebb1605262935b4b3ab446a95b1fad8a7d18098943dd548c8a486ef13.yml +openapi_spec_hash: 047517d5a996009459c04f2fe3b0d3f9 +config_hash: 5a44f3aad0ead6985fbdf0294c79286b diff --git a/api.md b/api.md index 01454851..f03f5609 100644 --- a/api.md +++ b/api.md @@ -11,6 +11,7 @@ Methods: - client.contexts.create(\*\*params) -> ContextCreateResponse - client.contexts.retrieve(id) -> ContextRetrieveResponse - client.contexts.update(id) -> ContextUpdateResponse +- client.contexts.delete(id) -> None # Extensions @@ -67,6 +68,7 @@ Methods: Methods: - client.sessions.downloads.list(id) -> BinaryAPIResponse +- client.sessions.downloads.delete(id) -> None ## Logs diff --git a/src/browserbase/resources/contexts.py b/src/browserbase/resources/contexts.py index bc4d1cc8..e2335007 100644 --- a/src/browserbase/resources/contexts.py +++ b/src/browserbase/resources/contexts.py @@ -5,7 +5,7 @@ import httpx from ..types import context_create_params -from .._types import NOT_GIVEN, Body, Query, Headers, NotGiven +from .._types import NOT_GIVEN, Body, Query, Headers, NoneType, NotGiven from .._utils import maybe_transform, async_maybe_transform from .._compat import cached_property from .._resource import SyncAPIResource, AsyncAPIResource @@ -145,6 +145,40 @@ def update( cast_to=ContextUpdateResponse, ) + def delete( + self, + 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, + ) -> None: + """ + Delete a Context + + 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 id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + extra_headers = {"Accept": "*/*", **(extra_headers or {})} + return self._delete( + f"/v1/contexts/{id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=NoneType, + ) + class AsyncContextsResource(AsyncAPIResource): @cached_property @@ -268,6 +302,40 @@ async def update( cast_to=ContextUpdateResponse, ) + async def delete( + self, + 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, + ) -> None: + """ + Delete a Context + + 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 id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + extra_headers = {"Accept": "*/*", **(extra_headers or {})} + return await self._delete( + f"/v1/contexts/{id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=NoneType, + ) + class ContextsResourceWithRawResponse: def __init__(self, contexts: ContextsResource) -> None: @@ -282,6 +350,9 @@ def __init__(self, contexts: ContextsResource) -> None: self.update = to_raw_response_wrapper( contexts.update, ) + self.delete = to_raw_response_wrapper( + contexts.delete, + ) class AsyncContextsResourceWithRawResponse: @@ -297,6 +368,9 @@ def __init__(self, contexts: AsyncContextsResource) -> None: self.update = async_to_raw_response_wrapper( contexts.update, ) + self.delete = async_to_raw_response_wrapper( + contexts.delete, + ) class ContextsResourceWithStreamingResponse: @@ -312,6 +386,9 @@ def __init__(self, contexts: ContextsResource) -> None: self.update = to_streamed_response_wrapper( contexts.update, ) + self.delete = to_streamed_response_wrapper( + contexts.delete, + ) class AsyncContextsResourceWithStreamingResponse: @@ -327,3 +404,6 @@ def __init__(self, contexts: AsyncContextsResource) -> None: self.update = async_to_streamed_response_wrapper( contexts.update, ) + self.delete = async_to_streamed_response_wrapper( + contexts.delete, + ) diff --git a/src/browserbase/resources/sessions/downloads.py b/src/browserbase/resources/sessions/downloads.py index 9ee49759..88acf474 100644 --- a/src/browserbase/resources/sessions/downloads.py +++ b/src/browserbase/resources/sessions/downloads.py @@ -4,7 +4,7 @@ import httpx -from ..._types import NOT_GIVEN, Body, Query, Headers, NotGiven +from ..._types import NOT_GIVEN, Body, Query, Headers, NoneType, NotGiven from ..._compat import cached_property from ..._resource import SyncAPIResource, AsyncAPIResource from ..._response import ( @@ -12,7 +12,11 @@ AsyncBinaryAPIResponse, StreamedBinaryAPIResponse, AsyncStreamedBinaryAPIResponse, + to_raw_response_wrapper, + to_streamed_response_wrapper, + async_to_raw_response_wrapper, to_custom_raw_response_wrapper, + async_to_streamed_response_wrapper, to_custom_streamed_response_wrapper, async_to_custom_raw_response_wrapper, async_to_custom_streamed_response_wrapper, @@ -76,6 +80,40 @@ def list( cast_to=BinaryAPIResponse, ) + def delete( + self, + 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, + ) -> None: + """ + Delete Session Downloads + + 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 id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + extra_headers = {"Accept": "*/*", **(extra_headers or {})} + return self._delete( + f"/v1/sessions/{id}/downloads", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=NoneType, + ) + class AsyncDownloadsResource(AsyncAPIResource): @cached_property @@ -131,6 +169,40 @@ async def list( cast_to=AsyncBinaryAPIResponse, ) + async def delete( + self, + 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, + ) -> None: + """ + Delete Session Downloads + + 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 id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + extra_headers = {"Accept": "*/*", **(extra_headers or {})} + return await self._delete( + f"/v1/sessions/{id}/downloads", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=NoneType, + ) + class DownloadsResourceWithRawResponse: def __init__(self, downloads: DownloadsResource) -> None: @@ -140,6 +212,9 @@ def __init__(self, downloads: DownloadsResource) -> None: downloads.list, BinaryAPIResponse, ) + self.delete = to_raw_response_wrapper( + downloads.delete, + ) class AsyncDownloadsResourceWithRawResponse: @@ -150,6 +225,9 @@ def __init__(self, downloads: AsyncDownloadsResource) -> None: downloads.list, AsyncBinaryAPIResponse, ) + self.delete = async_to_raw_response_wrapper( + downloads.delete, + ) class DownloadsResourceWithStreamingResponse: @@ -160,6 +238,9 @@ def __init__(self, downloads: DownloadsResource) -> None: downloads.list, StreamedBinaryAPIResponse, ) + self.delete = to_streamed_response_wrapper( + downloads.delete, + ) class AsyncDownloadsResourceWithStreamingResponse: @@ -170,3 +251,6 @@ def __init__(self, downloads: AsyncDownloadsResource) -> None: downloads.list, AsyncStreamedBinaryAPIResponse, ) + self.delete = async_to_streamed_response_wrapper( + downloads.delete, + ) diff --git a/src/browserbase/types/session_create_params.py b/src/browserbase/types/session_create_params.py index 3c96dd03..3a517c06 100644 --- a/src/browserbase/types/session_create_params.py +++ b/src/browserbase/types/session_create_params.py @@ -144,6 +144,12 @@ class BrowserSettings(TypedDict, total=False): log_session: Annotated[bool, PropertyInfo(alias="logSession")] """Enable or disable session logging. Defaults to `true`.""" + os: Literal["windows", "mac", "linux", "mobile", "tablet"] + """Operating system for stealth mode. + + Valid values: windows, mac, linux, mobile, tablet + """ + record_session: Annotated[bool, PropertyInfo(alias="recordSession")] """Enable or disable session recording. Defaults to `true`.""" diff --git a/tests/api_resources/sessions/test_downloads.py b/tests/api_resources/sessions/test_downloads.py index 10e84fdb..1f65d65f 100644 --- a/tests/api_resources/sessions/test_downloads.py +++ b/tests/api_resources/sessions/test_downloads.py @@ -73,6 +73,44 @@ def test_path_params_list(self, client: Browserbase) -> None: "", ) + @parametrize + def test_method_delete(self, client: Browserbase) -> None: + download = client.sessions.downloads.delete( + "id", + ) + assert download is None + + @parametrize + def test_raw_response_delete(self, client: Browserbase) -> None: + response = client.sessions.downloads.with_raw_response.delete( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + download = response.parse() + assert download is None + + @parametrize + def test_streaming_response_delete(self, client: Browserbase) -> None: + with client.sessions.downloads.with_streaming_response.delete( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + download = response.parse() + assert download is None + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_delete(self, client: Browserbase) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.sessions.downloads.with_raw_response.delete( + "", + ) + class TestAsyncDownloads: parametrize = pytest.mark.parametrize( @@ -128,3 +166,41 @@ async def test_path_params_list(self, async_client: AsyncBrowserbase) -> None: await async_client.sessions.downloads.with_raw_response.list( "", ) + + @parametrize + async def test_method_delete(self, async_client: AsyncBrowserbase) -> None: + download = await async_client.sessions.downloads.delete( + "id", + ) + assert download is None + + @parametrize + async def test_raw_response_delete(self, async_client: AsyncBrowserbase) -> None: + response = await async_client.sessions.downloads.with_raw_response.delete( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + download = await response.parse() + assert download is None + + @parametrize + async def test_streaming_response_delete(self, async_client: AsyncBrowserbase) -> None: + async with async_client.sessions.downloads.with_streaming_response.delete( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + download = await response.parse() + assert download is None + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_delete(self, async_client: AsyncBrowserbase) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.sessions.downloads.with_raw_response.delete( + "", + ) diff --git a/tests/api_resources/test_contexts.py b/tests/api_resources/test_contexts.py index 4ad27733..d32ebc4b 100644 --- a/tests/api_resources/test_contexts.py +++ b/tests/api_resources/test_contexts.py @@ -128,6 +128,44 @@ def test_path_params_update(self, client: Browserbase) -> None: "", ) + @parametrize + def test_method_delete(self, client: Browserbase) -> None: + context = client.contexts.delete( + "id", + ) + assert context is None + + @parametrize + def test_raw_response_delete(self, client: Browserbase) -> None: + response = client.contexts.with_raw_response.delete( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + context = response.parse() + assert context is None + + @parametrize + def test_streaming_response_delete(self, client: Browserbase) -> None: + with client.contexts.with_streaming_response.delete( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + context = response.parse() + assert context is None + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_delete(self, client: Browserbase) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.contexts.with_raw_response.delete( + "", + ) + class TestAsyncContexts: parametrize = pytest.mark.parametrize( @@ -240,3 +278,41 @@ async def test_path_params_update(self, async_client: AsyncBrowserbase) -> None: await async_client.contexts.with_raw_response.update( "", ) + + @parametrize + async def test_method_delete(self, async_client: AsyncBrowserbase) -> None: + context = await async_client.contexts.delete( + "id", + ) + assert context is None + + @parametrize + async def test_raw_response_delete(self, async_client: AsyncBrowserbase) -> None: + response = await async_client.contexts.with_raw_response.delete( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + context = await response.parse() + assert context is None + + @parametrize + async def test_streaming_response_delete(self, async_client: AsyncBrowserbase) -> None: + async with async_client.contexts.with_streaming_response.delete( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + context = await response.parse() + assert context is None + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_delete(self, async_client: AsyncBrowserbase) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.contexts.with_raw_response.delete( + "", + ) diff --git a/tests/api_resources/test_sessions.py b/tests/api_resources/test_sessions.py index 1838a8a1..7a16f64f 100644 --- a/tests/api_resources/test_sessions.py +++ b/tests/api_resources/test_sessions.py @@ -58,6 +58,7 @@ def test_method_create_with_all_params(self, client: Browserbase) -> None: }, }, "log_session": True, + "os": "windows", "record_session": True, "solve_captchas": True, "viewport": { @@ -304,6 +305,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncBrowserbas }, }, "log_session": True, + "os": "windows", "record_session": True, "solve_captchas": True, "viewport": { From 3fff64d1502e4196872e004dcce4a3f0c3c745a2 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 4 Sep 2025 09:40:43 +0000 Subject: [PATCH 193/330] feat(api): api update --- .stats.yml | 8 +- api.md | 2 - src/browserbase/resources/contexts.py | 82 +----------------- .../resources/sessions/downloads.py | 86 +------------------ .../types/session_create_params.py | 6 -- .../api_resources/sessions/test_downloads.py | 76 ---------------- tests/api_resources/test_contexts.py | 76 ---------------- tests/api_resources/test_sessions.py | 2 - 8 files changed, 6 insertions(+), 332 deletions(-) diff --git a/.stats.yml b/.stats.yml index 05e31b2a..fc1eb92e 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ -configured_endpoints: 20 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fbrowserbase-be7a4aeebb1605262935b4b3ab446a95b1fad8a7d18098943dd548c8a486ef13.yml -openapi_spec_hash: 047517d5a996009459c04f2fe3b0d3f9 -config_hash: 5a44f3aad0ead6985fbdf0294c79286b +configured_endpoints: 18 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fbrowserbase-c14a7d6b23a7fd42a26a7c55a668d1dcd2e4b58354b878e696bc959d808c71c9.yml +openapi_spec_hash: a0878bab95e435f9ce0d2418f0784d06 +config_hash: b3ca4ec5b02e5333af51ebc2e9fdef1b diff --git a/api.md b/api.md index f03f5609..01454851 100644 --- a/api.md +++ b/api.md @@ -11,7 +11,6 @@ Methods: - client.contexts.create(\*\*params) -> ContextCreateResponse - client.contexts.retrieve(id) -> ContextRetrieveResponse - client.contexts.update(id) -> ContextUpdateResponse -- client.contexts.delete(id) -> None # Extensions @@ -68,7 +67,6 @@ Methods: Methods: - client.sessions.downloads.list(id) -> BinaryAPIResponse -- client.sessions.downloads.delete(id) -> None ## Logs diff --git a/src/browserbase/resources/contexts.py b/src/browserbase/resources/contexts.py index e2335007..bc4d1cc8 100644 --- a/src/browserbase/resources/contexts.py +++ b/src/browserbase/resources/contexts.py @@ -5,7 +5,7 @@ import httpx from ..types import context_create_params -from .._types import NOT_GIVEN, Body, Query, Headers, NoneType, NotGiven +from .._types import NOT_GIVEN, Body, Query, Headers, NotGiven from .._utils import maybe_transform, async_maybe_transform from .._compat import cached_property from .._resource import SyncAPIResource, AsyncAPIResource @@ -145,40 +145,6 @@ def update( cast_to=ContextUpdateResponse, ) - def delete( - self, - 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, - ) -> None: - """ - Delete a Context - - 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 id: - raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") - extra_headers = {"Accept": "*/*", **(extra_headers or {})} - return self._delete( - f"/v1/contexts/{id}", - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=NoneType, - ) - class AsyncContextsResource(AsyncAPIResource): @cached_property @@ -302,40 +268,6 @@ async def update( cast_to=ContextUpdateResponse, ) - async def delete( - self, - 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, - ) -> None: - """ - Delete a Context - - 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 id: - raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") - extra_headers = {"Accept": "*/*", **(extra_headers or {})} - return await self._delete( - f"/v1/contexts/{id}", - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=NoneType, - ) - class ContextsResourceWithRawResponse: def __init__(self, contexts: ContextsResource) -> None: @@ -350,9 +282,6 @@ def __init__(self, contexts: ContextsResource) -> None: self.update = to_raw_response_wrapper( contexts.update, ) - self.delete = to_raw_response_wrapper( - contexts.delete, - ) class AsyncContextsResourceWithRawResponse: @@ -368,9 +297,6 @@ def __init__(self, contexts: AsyncContextsResource) -> None: self.update = async_to_raw_response_wrapper( contexts.update, ) - self.delete = async_to_raw_response_wrapper( - contexts.delete, - ) class ContextsResourceWithStreamingResponse: @@ -386,9 +312,6 @@ def __init__(self, contexts: ContextsResource) -> None: self.update = to_streamed_response_wrapper( contexts.update, ) - self.delete = to_streamed_response_wrapper( - contexts.delete, - ) class AsyncContextsResourceWithStreamingResponse: @@ -404,6 +327,3 @@ def __init__(self, contexts: AsyncContextsResource) -> None: self.update = async_to_streamed_response_wrapper( contexts.update, ) - self.delete = async_to_streamed_response_wrapper( - contexts.delete, - ) diff --git a/src/browserbase/resources/sessions/downloads.py b/src/browserbase/resources/sessions/downloads.py index 88acf474..9ee49759 100644 --- a/src/browserbase/resources/sessions/downloads.py +++ b/src/browserbase/resources/sessions/downloads.py @@ -4,7 +4,7 @@ import httpx -from ..._types import NOT_GIVEN, Body, Query, Headers, NoneType, NotGiven +from ..._types import NOT_GIVEN, Body, Query, Headers, NotGiven from ..._compat import cached_property from ..._resource import SyncAPIResource, AsyncAPIResource from ..._response import ( @@ -12,11 +12,7 @@ AsyncBinaryAPIResponse, StreamedBinaryAPIResponse, AsyncStreamedBinaryAPIResponse, - to_raw_response_wrapper, - to_streamed_response_wrapper, - async_to_raw_response_wrapper, to_custom_raw_response_wrapper, - async_to_streamed_response_wrapper, to_custom_streamed_response_wrapper, async_to_custom_raw_response_wrapper, async_to_custom_streamed_response_wrapper, @@ -80,40 +76,6 @@ def list( cast_to=BinaryAPIResponse, ) - def delete( - self, - 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, - ) -> None: - """ - Delete Session Downloads - - 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 id: - raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") - extra_headers = {"Accept": "*/*", **(extra_headers or {})} - return self._delete( - f"/v1/sessions/{id}/downloads", - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=NoneType, - ) - class AsyncDownloadsResource(AsyncAPIResource): @cached_property @@ -169,40 +131,6 @@ async def list( cast_to=AsyncBinaryAPIResponse, ) - async def delete( - self, - 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, - ) -> None: - """ - Delete Session Downloads - - 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 id: - raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") - extra_headers = {"Accept": "*/*", **(extra_headers or {})} - return await self._delete( - f"/v1/sessions/{id}/downloads", - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=NoneType, - ) - class DownloadsResourceWithRawResponse: def __init__(self, downloads: DownloadsResource) -> None: @@ -212,9 +140,6 @@ def __init__(self, downloads: DownloadsResource) -> None: downloads.list, BinaryAPIResponse, ) - self.delete = to_raw_response_wrapper( - downloads.delete, - ) class AsyncDownloadsResourceWithRawResponse: @@ -225,9 +150,6 @@ def __init__(self, downloads: AsyncDownloadsResource) -> None: downloads.list, AsyncBinaryAPIResponse, ) - self.delete = async_to_raw_response_wrapper( - downloads.delete, - ) class DownloadsResourceWithStreamingResponse: @@ -238,9 +160,6 @@ def __init__(self, downloads: DownloadsResource) -> None: downloads.list, StreamedBinaryAPIResponse, ) - self.delete = to_streamed_response_wrapper( - downloads.delete, - ) class AsyncDownloadsResourceWithStreamingResponse: @@ -251,6 +170,3 @@ def __init__(self, downloads: AsyncDownloadsResource) -> None: downloads.list, AsyncStreamedBinaryAPIResponse, ) - self.delete = async_to_streamed_response_wrapper( - downloads.delete, - ) diff --git a/src/browserbase/types/session_create_params.py b/src/browserbase/types/session_create_params.py index 3a517c06..3c96dd03 100644 --- a/src/browserbase/types/session_create_params.py +++ b/src/browserbase/types/session_create_params.py @@ -144,12 +144,6 @@ class BrowserSettings(TypedDict, total=False): log_session: Annotated[bool, PropertyInfo(alias="logSession")] """Enable or disable session logging. Defaults to `true`.""" - os: Literal["windows", "mac", "linux", "mobile", "tablet"] - """Operating system for stealth mode. - - Valid values: windows, mac, linux, mobile, tablet - """ - record_session: Annotated[bool, PropertyInfo(alias="recordSession")] """Enable or disable session recording. Defaults to `true`.""" diff --git a/tests/api_resources/sessions/test_downloads.py b/tests/api_resources/sessions/test_downloads.py index 1f65d65f..10e84fdb 100644 --- a/tests/api_resources/sessions/test_downloads.py +++ b/tests/api_resources/sessions/test_downloads.py @@ -73,44 +73,6 @@ def test_path_params_list(self, client: Browserbase) -> None: "", ) - @parametrize - def test_method_delete(self, client: Browserbase) -> None: - download = client.sessions.downloads.delete( - "id", - ) - assert download is None - - @parametrize - def test_raw_response_delete(self, client: Browserbase) -> None: - response = client.sessions.downloads.with_raw_response.delete( - "id", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - download = response.parse() - assert download is None - - @parametrize - def test_streaming_response_delete(self, client: Browserbase) -> None: - with client.sessions.downloads.with_streaming_response.delete( - "id", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - download = response.parse() - assert download is None - - assert cast(Any, response.is_closed) is True - - @parametrize - def test_path_params_delete(self, client: Browserbase) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): - client.sessions.downloads.with_raw_response.delete( - "", - ) - class TestAsyncDownloads: parametrize = pytest.mark.parametrize( @@ -166,41 +128,3 @@ async def test_path_params_list(self, async_client: AsyncBrowserbase) -> None: await async_client.sessions.downloads.with_raw_response.list( "", ) - - @parametrize - async def test_method_delete(self, async_client: AsyncBrowserbase) -> None: - download = await async_client.sessions.downloads.delete( - "id", - ) - assert download is None - - @parametrize - async def test_raw_response_delete(self, async_client: AsyncBrowserbase) -> None: - response = await async_client.sessions.downloads.with_raw_response.delete( - "id", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - download = await response.parse() - assert download is None - - @parametrize - async def test_streaming_response_delete(self, async_client: AsyncBrowserbase) -> None: - async with async_client.sessions.downloads.with_streaming_response.delete( - "id", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - download = await response.parse() - assert download is None - - assert cast(Any, response.is_closed) is True - - @parametrize - async def test_path_params_delete(self, async_client: AsyncBrowserbase) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): - await async_client.sessions.downloads.with_raw_response.delete( - "", - ) diff --git a/tests/api_resources/test_contexts.py b/tests/api_resources/test_contexts.py index d32ebc4b..4ad27733 100644 --- a/tests/api_resources/test_contexts.py +++ b/tests/api_resources/test_contexts.py @@ -128,44 +128,6 @@ def test_path_params_update(self, client: Browserbase) -> None: "", ) - @parametrize - def test_method_delete(self, client: Browserbase) -> None: - context = client.contexts.delete( - "id", - ) - assert context is None - - @parametrize - def test_raw_response_delete(self, client: Browserbase) -> None: - response = client.contexts.with_raw_response.delete( - "id", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - context = response.parse() - assert context is None - - @parametrize - def test_streaming_response_delete(self, client: Browserbase) -> None: - with client.contexts.with_streaming_response.delete( - "id", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - context = response.parse() - assert context is None - - assert cast(Any, response.is_closed) is True - - @parametrize - def test_path_params_delete(self, client: Browserbase) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): - client.contexts.with_raw_response.delete( - "", - ) - class TestAsyncContexts: parametrize = pytest.mark.parametrize( @@ -278,41 +240,3 @@ async def test_path_params_update(self, async_client: AsyncBrowserbase) -> None: await async_client.contexts.with_raw_response.update( "", ) - - @parametrize - async def test_method_delete(self, async_client: AsyncBrowserbase) -> None: - context = await async_client.contexts.delete( - "id", - ) - assert context is None - - @parametrize - async def test_raw_response_delete(self, async_client: AsyncBrowserbase) -> None: - response = await async_client.contexts.with_raw_response.delete( - "id", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - context = await response.parse() - assert context is None - - @parametrize - async def test_streaming_response_delete(self, async_client: AsyncBrowserbase) -> None: - async with async_client.contexts.with_streaming_response.delete( - "id", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - context = await response.parse() - assert context is None - - assert cast(Any, response.is_closed) is True - - @parametrize - async def test_path_params_delete(self, async_client: AsyncBrowserbase) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): - await async_client.contexts.with_raw_response.delete( - "", - ) diff --git a/tests/api_resources/test_sessions.py b/tests/api_resources/test_sessions.py index 7a16f64f..1838a8a1 100644 --- a/tests/api_resources/test_sessions.py +++ b/tests/api_resources/test_sessions.py @@ -58,7 +58,6 @@ def test_method_create_with_all_params(self, client: Browserbase) -> None: }, }, "log_session": True, - "os": "windows", "record_session": True, "solve_captchas": True, "viewport": { @@ -305,7 +304,6 @@ async def test_method_create_with_all_params(self, async_client: AsyncBrowserbas }, }, "log_session": True, - "os": "windows", "record_session": True, "solve_captchas": True, "viewport": { From 69ef0a49728e857a6682f69133136a6f03c7ebfd Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 4 Sep 2025 18:37:37 +0000 Subject: [PATCH 194/330] feat(api): api update --- .stats.yml | 4 ++-- src/browserbase/types/session_create_params.py | 6 ++++++ tests/api_resources/test_sessions.py | 2 ++ 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index fc1eb92e..b000c8c1 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 18 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fbrowserbase-c14a7d6b23a7fd42a26a7c55a668d1dcd2e4b58354b878e696bc959d808c71c9.yml -openapi_spec_hash: a0878bab95e435f9ce0d2418f0784d06 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fbrowserbase-be7a4aeebb1605262935b4b3ab446a95b1fad8a7d18098943dd548c8a486ef13.yml +openapi_spec_hash: 1c950a109f80140711e7ae2cf87fddad config_hash: b3ca4ec5b02e5333af51ebc2e9fdef1b diff --git a/src/browserbase/types/session_create_params.py b/src/browserbase/types/session_create_params.py index 3c96dd03..3a517c06 100644 --- a/src/browserbase/types/session_create_params.py +++ b/src/browserbase/types/session_create_params.py @@ -144,6 +144,12 @@ class BrowserSettings(TypedDict, total=False): log_session: Annotated[bool, PropertyInfo(alias="logSession")] """Enable or disable session logging. Defaults to `true`.""" + os: Literal["windows", "mac", "linux", "mobile", "tablet"] + """Operating system for stealth mode. + + Valid values: windows, mac, linux, mobile, tablet + """ + record_session: Annotated[bool, PropertyInfo(alias="recordSession")] """Enable or disable session recording. Defaults to `true`.""" diff --git a/tests/api_resources/test_sessions.py b/tests/api_resources/test_sessions.py index 1838a8a1..7a16f64f 100644 --- a/tests/api_resources/test_sessions.py +++ b/tests/api_resources/test_sessions.py @@ -58,6 +58,7 @@ def test_method_create_with_all_params(self, client: Browserbase) -> None: }, }, "log_session": True, + "os": "windows", "record_session": True, "solve_captchas": True, "viewport": { @@ -304,6 +305,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncBrowserbas }, }, "log_session": True, + "os": "windows", "record_session": True, "solve_captchas": True, "viewport": { From ea10127a5d99f3de94ccad7a27404be3682d76db Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 4 Sep 2025 23:58:40 +0000 Subject: [PATCH 195/330] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index b000c8c1..13a5d846 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 18 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fbrowserbase-be7a4aeebb1605262935b4b3ab446a95b1fad8a7d18098943dd548c8a486ef13.yml openapi_spec_hash: 1c950a109f80140711e7ae2cf87fddad -config_hash: b3ca4ec5b02e5333af51ebc2e9fdef1b +config_hash: ec077c0d8cde29588ca4ff30d49575a4 From 29136184f85e7d7b47312fc96ace7879f0efecc3 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 5 Sep 2025 00:54:25 +0000 Subject: [PATCH 196/330] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index 13a5d846..b000c8c1 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 18 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fbrowserbase-be7a4aeebb1605262935b4b3ab446a95b1fad8a7d18098943dd548c8a486ef13.yml openapi_spec_hash: 1c950a109f80140711e7ae2cf87fddad -config_hash: ec077c0d8cde29588ca4ff30d49575a4 +config_hash: b3ca4ec5b02e5333af51ebc2e9fdef1b From 9f8eda4fe4cf000c25bc2cb60e7b06aba64f4523 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 5 Sep 2025 01:04:02 +0000 Subject: [PATCH 197/330] feat(api): manual updates --- .stats.yml | 6 +++--- src/browserbase/types/session_create_params.py | 3 +++ tests/api_resources/test_sessions.py | 2 ++ 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/.stats.yml b/.stats.yml index b000c8c1..a50ccc0a 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 18 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fbrowserbase-be7a4aeebb1605262935b4b3ab446a95b1fad8a7d18098943dd548c8a486ef13.yml -openapi_spec_hash: 1c950a109f80140711e7ae2cf87fddad -config_hash: b3ca4ec5b02e5333af51ebc2e9fdef1b +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fbrowserbase-a9ab6f9017f7645722d220eb8172516a7a5400e86542c28fc7e121adcd1f344f.yml +openapi_spec_hash: e29347aba2697d4efa3dce7794810dbd +config_hash: ec077c0d8cde29588ca4ff30d49575a4 diff --git a/src/browserbase/types/session_create_params.py b/src/browserbase/types/session_create_params.py index 3a517c06..31a08ceb 100644 --- a/src/browserbase/types/session_create_params.py +++ b/src/browserbase/types/session_create_params.py @@ -141,6 +141,9 @@ class BrowserSettings(TypedDict, total=False): [on the Stealth Mode page](/features/stealth-mode#fingerprinting) """ + headful: bool + """[NOT IN DOCS] Enable or disable headful mode. Defaults to `false`.""" + log_session: Annotated[bool, PropertyInfo(alias="logSession")] """Enable or disable session logging. Defaults to `true`.""" diff --git a/tests/api_resources/test_sessions.py b/tests/api_resources/test_sessions.py index 7a16f64f..d7d6a903 100644 --- a/tests/api_resources/test_sessions.py +++ b/tests/api_resources/test_sessions.py @@ -57,6 +57,7 @@ def test_method_create_with_all_params(self, client: Browserbase) -> None: "min_width": 0, }, }, + "headful": True, "log_session": True, "os": "windows", "record_session": True, @@ -304,6 +305,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncBrowserbas "min_width": 0, }, }, + "headful": True, "log_session": True, "os": "windows", "record_session": True, From 2f31f7c41148083526cb806bd89f6d264776cf87 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 5 Sep 2025 01:06:42 +0000 Subject: [PATCH 198/330] chore(internal): version bump --- .release-please-manifest.json | 2 +- README.md | 4 ++-- pyproject.toml | 2 +- src/browserbase/_version.py | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 3e9af1b3..7a22c4a0 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "1.4.0" + ".": "1.5.0-alpha.0" } \ No newline at end of file diff --git a/README.md b/README.md index 726c766d..4edddc3e 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ The REST API documentation can be found on [docs.browserbase.com](https://docs.b ```sh # install from PyPI -pip install browserbase +pip install --pre browserbase ``` ## Usage @@ -77,7 +77,7 @@ You can enable this by installing `aiohttp`: ```sh # install from PyPI -pip install browserbase[aiohttp] +pip install --pre browserbase[aiohttp] ``` Then you can enable it by instantiating the client with `http_client=DefaultAioHttpClient()`: diff --git a/pyproject.toml b/pyproject.toml index 40f7993a..d5590195 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "browserbase" -version = "1.4.0" +version = "1.5.0-alpha.0" description = "The official Python library for the Browserbase API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/browserbase/_version.py b/src/browserbase/_version.py index 3c0492ea..25f19565 100644 --- a/src/browserbase/_version.py +++ b/src/browserbase/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "browserbase" -__version__ = "1.4.0" # x-release-please-version +__version__ = "1.5.0-alpha.0" # x-release-please-version From d9fb0fa2a544db849a1f902a5c59b23c716d8c97 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 5 Sep 2025 01:11:40 +0000 Subject: [PATCH 199/330] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index a50ccc0a..d907a8cd 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 18 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fbrowserbase-a9ab6f9017f7645722d220eb8172516a7a5400e86542c28fc7e121adcd1f344f.yml openapi_spec_hash: e29347aba2697d4efa3dce7794810dbd -config_hash: ec077c0d8cde29588ca4ff30d49575a4 +config_hash: 72e1030e3b7f40188575d7b06f850923 From f0bf6dfc9ee49f41afdf5e5b5008cc880c9e1028 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 5 Sep 2025 04:21:14 +0000 Subject: [PATCH 200/330] chore(internal): move mypy configurations to `pyproject.toml` file --- mypy.ini | 50 ------------------------------------------------ pyproject.toml | 52 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 50 deletions(-) delete mode 100644 mypy.ini diff --git a/mypy.ini b/mypy.ini deleted file mode 100644 index 811af717..00000000 --- a/mypy.ini +++ /dev/null @@ -1,50 +0,0 @@ -[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/browserbase/_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 -[mypy.overrides] -module = "black.files.*" -ignore_errors = true -ignore_missing_imports = true diff --git a/pyproject.toml b/pyproject.toml index d5590195..1c289d75 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -157,6 +157,58 @@ 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/browserbase/_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" From 0027b8f3feae3d2a755dc33bb895cc1afe167ccb Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 5 Sep 2025 17:22:07 +0000 Subject: [PATCH 201/330] feat(api): api update --- .stats.yml | 6 +++--- src/browserbase/types/session_create_params.py | 3 --- tests/api_resources/test_sessions.py | 2 -- 3 files changed, 3 insertions(+), 8 deletions(-) diff --git a/.stats.yml b/.stats.yml index d907a8cd..b000c8c1 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 18 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fbrowserbase-a9ab6f9017f7645722d220eb8172516a7a5400e86542c28fc7e121adcd1f344f.yml -openapi_spec_hash: e29347aba2697d4efa3dce7794810dbd -config_hash: 72e1030e3b7f40188575d7b06f850923 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fbrowserbase-be7a4aeebb1605262935b4b3ab446a95b1fad8a7d18098943dd548c8a486ef13.yml +openapi_spec_hash: 1c950a109f80140711e7ae2cf87fddad +config_hash: b3ca4ec5b02e5333af51ebc2e9fdef1b diff --git a/src/browserbase/types/session_create_params.py b/src/browserbase/types/session_create_params.py index 31a08ceb..3a517c06 100644 --- a/src/browserbase/types/session_create_params.py +++ b/src/browserbase/types/session_create_params.py @@ -141,9 +141,6 @@ class BrowserSettings(TypedDict, total=False): [on the Stealth Mode page](/features/stealth-mode#fingerprinting) """ - headful: bool - """[NOT IN DOCS] Enable or disable headful mode. Defaults to `false`.""" - log_session: Annotated[bool, PropertyInfo(alias="logSession")] """Enable or disable session logging. Defaults to `true`.""" diff --git a/tests/api_resources/test_sessions.py b/tests/api_resources/test_sessions.py index d7d6a903..7a16f64f 100644 --- a/tests/api_resources/test_sessions.py +++ b/tests/api_resources/test_sessions.py @@ -57,7 +57,6 @@ def test_method_create_with_all_params(self, client: Browserbase) -> None: "min_width": 0, }, }, - "headful": True, "log_session": True, "os": "windows", "record_session": True, @@ -305,7 +304,6 @@ async def test_method_create_with_all_params(self, async_client: AsyncBrowserbas "min_width": 0, }, }, - "headful": True, "log_session": True, "os": "windows", "record_session": True, From ba68d4644b3e98e8a5286fb7df42ab51a6d87d1e Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 6 Sep 2025 04:56:29 +0000 Subject: [PATCH 202/330] chore(tests): simplify `get_platform` test `nest_asyncio` is archived and broken on some platforms so it's not worth keeping in our test suite. --- pyproject.toml | 1 - requirements-dev.lock | 1 - tests/test_client.py | 53 +++++-------------------------------------- 3 files changed, 6 insertions(+), 49 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 1c289d75..9d142ec4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -56,7 +56,6 @@ dev-dependencies = [ "dirty-equals>=0.6.0", "importlib-metadata>=6.7.0", "rich>=13.7.1", - "nest_asyncio==1.6.0", "pytest-xdist>=3.6.1", ] diff --git a/requirements-dev.lock b/requirements-dev.lock index ee261e52..6d623186 100644 --- a/requirements-dev.lock +++ b/requirements-dev.lock @@ -75,7 +75,6 @@ multidict==6.4.4 mypy==1.14.1 mypy-extensions==1.0.0 # via mypy -nest-asyncio==1.6.0 nodeenv==1.8.0 # via pyright nox==2023.4.22 diff --git a/tests/test_client.py b/tests/test_client.py index bf058259..aed68baf 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -6,13 +6,10 @@ import os import sys import json -import time import asyncio import inspect -import subprocess import tracemalloc from typing import Any, Union, cast -from textwrap import dedent from unittest import mock from typing_extensions import Literal @@ -23,14 +20,17 @@ from browserbase import Browserbase, AsyncBrowserbase, APIResponseValidationError from browserbase._types import Omit +from browserbase._utils import asyncify from browserbase._models import BaseModel, FinalRequestOptions from browserbase._exceptions import APIStatusError, APITimeoutError, BrowserbaseError, APIResponseValidationError from browserbase._base_client import ( DEFAULT_TIMEOUT, HTTPX_DEFAULT_TIMEOUT, BaseClient, + OtherPlatform, DefaultHttpxClient, DefaultAsyncHttpxClient, + get_platform, make_request_options, ) @@ -1647,50 +1647,9 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: assert response.http_request.headers.get("x-stainless-retry-count") == "42" - def test_get_platform(self) -> None: - # A previous implementation of asyncify could leave threads unterminated when - # used with nest_asyncio. - # - # Since nest_asyncio.apply() is global and cannot be un-applied, this - # test is run in a separate process to avoid affecting other tests. - test_code = dedent(""" - import asyncio - import nest_asyncio - import threading - - from browserbase._utils import asyncify - from browserbase._base_client import get_platform - - async def test_main() -> None: - result = await asyncify(get_platform)() - print(result) - for thread in threading.enumerate(): - print(thread.name) - - nest_asyncio.apply() - asyncio.run(test_main()) - """) - with subprocess.Popen( - [sys.executable, "-c", test_code], - text=True, - ) as process: - timeout = 10 # seconds - - start_time = time.monotonic() - while True: - return_code = process.poll() - if return_code is not None: - if return_code != 0: - raise AssertionError("calling get_platform using asyncify resulted in a non-zero exit code") - - # success - break - - if time.monotonic() - start_time > timeout: - process.kill() - raise AssertionError("calling get_platform using asyncify resulted in a hung process") - - time.sleep(0.1) + 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 From fdb865072aa620bc1bd9b9477daf69318c4f6a38 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 17 Sep 2025 03:13:10 +0000 Subject: [PATCH 203/330] chore(internal): update pydantic dependency --- requirements-dev.lock | 7 +++++-- requirements.lock | 7 +++++-- src/browserbase/_models.py | 14 ++++++++++---- 3 files changed, 20 insertions(+), 8 deletions(-) diff --git a/requirements-dev.lock b/requirements-dev.lock index 6d623186..7045395d 100644 --- a/requirements-dev.lock +++ b/requirements-dev.lock @@ -88,9 +88,9 @@ pluggy==1.5.0 propcache==0.3.1 # via aiohttp # via yarl -pydantic==2.10.3 +pydantic==2.11.9 # via browserbase -pydantic-core==2.27.1 +pydantic-core==2.33.2 # via pydantic pygments==2.18.0 # via rich @@ -126,6 +126,9 @@ typing-extensions==4.12.2 # via pydantic # via pydantic-core # via pyright + # via typing-inspection +typing-inspection==0.4.1 + # via pydantic virtualenv==20.24.5 # via nox yarl==1.20.0 diff --git a/requirements.lock b/requirements.lock index 6f4c4c9e..2495a260 100644 --- a/requirements.lock +++ b/requirements.lock @@ -55,9 +55,9 @@ multidict==6.4.4 propcache==0.3.1 # via aiohttp # via yarl -pydantic==2.10.3 +pydantic==2.11.9 # via browserbase -pydantic-core==2.27.1 +pydantic-core==2.33.2 # via pydantic sniffio==1.3.0 # via anyio @@ -68,5 +68,8 @@ typing-extensions==4.12.2 # via multidict # via pydantic # via pydantic-core + # via typing-inspection +typing-inspection==0.4.1 + # via pydantic yarl==1.20.0 # via aiohttp diff --git a/src/browserbase/_models.py b/src/browserbase/_models.py index 3a6017ef..6a3cd1d2 100644 --- a/src/browserbase/_models.py +++ b/src/browserbase/_models.py @@ -256,7 +256,7 @@ def model_dump( mode: Literal["json", "python"] | str = "python", include: IncEx | None = None, exclude: IncEx | None = None, - by_alias: bool = False, + by_alias: bool | None = None, exclude_unset: bool = False, exclude_defaults: bool = False, exclude_none: bool = False, @@ -264,6 +264,7 @@ def model_dump( warnings: bool | Literal["none", "warn", "error"] = True, context: dict[str, Any] | None = None, serialize_as_any: bool = False, + fallback: Callable[[Any], Any] | None = None, ) -> dict[str, Any]: """Usage docs: https://docs.pydantic.dev/2.4/concepts/serialization/#modelmodel_dump @@ -295,10 +296,12 @@ def model_dump( 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") dumped = super().dict( # pyright: ignore[reportDeprecated] include=include, exclude=exclude, - by_alias=by_alias, + by_alias=by_alias if by_alias is not None else False, exclude_unset=exclude_unset, exclude_defaults=exclude_defaults, exclude_none=exclude_none, @@ -313,13 +316,14 @@ def model_dump_json( indent: int | None = None, include: IncEx | None = None, exclude: IncEx | None = None, - by_alias: bool = False, + by_alias: bool | None = None, exclude_unset: bool = False, exclude_defaults: bool = False, exclude_none: bool = False, round_trip: bool = False, warnings: bool | Literal["none", "warn", "error"] = True, context: dict[str, Any] | None = None, + 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 @@ -348,11 +352,13 @@ def model_dump_json( 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") return super().json( # type: ignore[reportDeprecated] indent=indent, include=include, exclude=exclude, - by_alias=by_alias, + by_alias=by_alias if by_alias is not None else False, exclude_unset=exclude_unset, exclude_defaults=exclude_defaults, exclude_none=exclude_none, From 92d16f92e18f0affa2fd2bf3fa3cc3c804c6aa0f Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 19 Sep 2025 03:32:23 +0000 Subject: [PATCH 204/330] chore(types): change optional parameter type from NotGiven to Omit --- src/browserbase/__init__.py | 4 +- src/browserbase/_base_client.py | 18 +++--- src/browserbase/_client.py | 16 ++--- src/browserbase/_qs.py | 14 ++--- src/browserbase/_types.py | 29 ++++++---- src/browserbase/_utils/_transform.py | 4 +- src/browserbase/_utils/_utils.py | 8 +-- src/browserbase/resources/contexts.py | 14 ++--- src/browserbase/resources/extensions.py | 14 ++--- src/browserbase/resources/projects.py | 14 ++--- .../resources/sessions/downloads.py | 6 +- src/browserbase/resources/sessions/logs.py | 6 +- .../resources/sessions/recording.py | 6 +- .../resources/sessions/sessions.py | 58 +++++++++---------- src/browserbase/resources/sessions/uploads.py | 6 +- tests/test_transform.py | 11 +++- 16 files changed, 122 insertions(+), 106 deletions(-) diff --git a/src/browserbase/__init__.py b/src/browserbase/__init__.py index 8e128845..a7356c1e 100644 --- a/src/browserbase/__init__.py +++ b/src/browserbase/__init__.py @@ -3,7 +3,7 @@ import typing as _t from . import types -from ._types import NOT_GIVEN, Omit, NoneType, NotGiven, Transport, ProxiesTypes +from ._types import NOT_GIVEN, Omit, NoneType, NotGiven, Transport, ProxiesTypes, omit, not_given from ._utils import file_from_path from ._client import ( Client, @@ -48,7 +48,9 @@ "ProxiesTypes", "NotGiven", "NOT_GIVEN", + "not_given", "Omit", + "omit", "BrowserbaseError", "APIError", "APIStatusError", diff --git a/src/browserbase/_base_client.py b/src/browserbase/_base_client.py index 89549337..2485e4e6 100644 --- a/src/browserbase/_base_client.py +++ b/src/browserbase/_base_client.py @@ -42,7 +42,6 @@ from ._qs import Querystring from ._files import to_httpx_files, async_to_httpx_files from ._types import ( - NOT_GIVEN, Body, Omit, Query, @@ -57,6 +56,7 @@ 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 @@ -145,9 +145,9 @@ def __init__( def __init__( self, *, - url: URL | NotGiven = NOT_GIVEN, - json: Body | NotGiven = NOT_GIVEN, - params: Query | NotGiven = NOT_GIVEN, + url: URL | NotGiven = not_given, + json: Body | NotGiven = not_given, + params: Query | NotGiven = not_given, ) -> None: self.url = url self.json = json @@ -595,7 +595,7 @@ def _maybe_override_cast_to(self, cast_to: type[ResponseT], options: FinalReques # 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) + 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) @@ -825,7 +825,7 @@ def __init__( version: str, base_url: str | URL, max_retries: int = DEFAULT_MAX_RETRIES, - timeout: float | Timeout | None | NotGiven = NOT_GIVEN, + 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, @@ -1356,7 +1356,7 @@ def __init__( base_url: str | URL, _strict_response_validation: bool, max_retries: int = DEFAULT_MAX_RETRIES, - timeout: float | Timeout | None | NotGiven = NOT_GIVEN, + 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, @@ -1818,8 +1818,8 @@ def make_request_options( 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, + 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 = {} diff --git a/src/browserbase/_client.py b/src/browserbase/_client.py index a7039a2a..8b54a5bb 100644 --- a/src/browserbase/_client.py +++ b/src/browserbase/_client.py @@ -3,7 +3,7 @@ from __future__ import annotations import os -from typing import Any, Union, Mapping +from typing import Any, Mapping from typing_extensions import Self, override import httpx @@ -11,13 +11,13 @@ from . import _exceptions from ._qs import Querystring from ._types import ( - NOT_GIVEN, Omit, Timeout, NotGiven, Transport, ProxiesTypes, RequestOptions, + not_given, ) from ._utils import is_given, get_async_library from ._version import __version__ @@ -59,7 +59,7 @@ def __init__( *, api_key: str | None = None, base_url: str | httpx.URL | None = None, - timeout: Union[float, Timeout, 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, @@ -137,9 +137,9 @@ def copy( *, api_key: str | None = None, base_url: str | httpx.URL | None = None, - timeout: float | Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | Timeout | None | NotGiven = not_given, http_client: httpx.Client | None = None, - max_retries: int | NotGiven = NOT_GIVEN, + 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, @@ -233,7 +233,7 @@ def __init__( *, api_key: str | None = None, base_url: str | httpx.URL | None = None, - timeout: Union[float, Timeout, 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, @@ -311,9 +311,9 @@ def copy( *, api_key: str | None = None, base_url: str | httpx.URL | None = None, - timeout: float | Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | Timeout | None | NotGiven = not_given, http_client: httpx.AsyncClient | None = None, - max_retries: int | NotGiven = NOT_GIVEN, + 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, diff --git a/src/browserbase/_qs.py b/src/browserbase/_qs.py index 274320ca..ada6fd3f 100644 --- a/src/browserbase/_qs.py +++ b/src/browserbase/_qs.py @@ -4,7 +4,7 @@ from urllib.parse import parse_qs, urlencode from typing_extensions import Literal, get_args -from ._types import NOT_GIVEN, NotGiven, NotGivenOr +from ._types import NotGiven, not_given from ._utils import flatten _T = TypeVar("_T") @@ -41,8 +41,8 @@ def stringify( self, params: Params, *, - array_format: NotGivenOr[ArrayFormat] = NOT_GIVEN, - nested_format: NotGivenOr[NestedFormat] = NOT_GIVEN, + array_format: ArrayFormat | NotGiven = not_given, + nested_format: NestedFormat | NotGiven = not_given, ) -> str: return urlencode( self.stringify_items( @@ -56,8 +56,8 @@ def stringify_items( self, params: Params, *, - array_format: NotGivenOr[ArrayFormat] = NOT_GIVEN, - nested_format: NotGivenOr[NestedFormat] = NOT_GIVEN, + array_format: ArrayFormat | NotGiven = not_given, + nested_format: NestedFormat | NotGiven = not_given, ) -> list[tuple[str, str]]: opts = Options( qs=self, @@ -143,8 +143,8 @@ def __init__( self, qs: Querystring = _qs, *, - array_format: NotGivenOr[ArrayFormat] = NOT_GIVEN, - nested_format: NotGivenOr[NestedFormat] = NOT_GIVEN, + 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/browserbase/_types.py b/src/browserbase/_types.py index b954306a..f86be54d 100644 --- a/src/browserbase/_types.py +++ b/src/browserbase/_types.py @@ -117,18 +117,21 @@ class RequestOptions(TypedDict, total=False): # Sentinel class used until PEP 0661 is accepted class NotGiven: """ - A sentinel singleton class used to distinguish omitted keyword arguments - from those passed in with the value None (which may have different behavior). + 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 get(timeout: Union[int, NotGiven, None] = NotGiven()) -> Response: ... + def create(timeout: Timeout | None | NotGiven = not_given): ... - get(timeout=1) # 1s timeout - get(timeout=None) # No timeout - get() # Default timeout behavior, which may not be statically known at the method definition. + create(timeout=1) # 1s timeout + create(timeout=None) # No timeout + create() # Default timeout behavior ``` """ @@ -140,13 +143,14 @@ def __repr__(self) -> str: return "NOT_GIVEN" -NotGivenOr = Union[_T, NotGiven] +not_given = NotGiven() +# for backwards compatibility: NOT_GIVEN = NotGiven() class Omit: - """In certain situations you need to be able to represent a case where a default value has - to be explicitly removed and `None` is not an appropriate substitute, for example: + """ + 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 @@ -156,8 +160,8 @@ class Omit: # 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()}) + # instead you can remove the default `application/json` header by passing omit + client.post(..., headers={"Content-Type": omit}) ``` """ @@ -165,6 +169,9 @@ def __bool__(self) -> Literal[False]: return False +omit = Omit() + + @runtime_checkable class ModelBuilderProtocol(Protocol): @classmethod diff --git a/src/browserbase/_utils/_transform.py b/src/browserbase/_utils/_transform.py index c19124f0..52075492 100644 --- a/src/browserbase/_utils/_transform.py +++ b/src/browserbase/_utils/_transform.py @@ -268,7 +268,7 @@ def _transform_typeddict( 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 `NotGiven` values here as they'll + # we don't need to include omitted values here as they'll # be stripped out before the request is sent anyway continue @@ -434,7 +434,7 @@ async def _async_transform_typeddict( 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 `NotGiven` values here as they'll + # we don't need to include omitted values here as they'll # be stripped out before the request is sent anyway continue diff --git a/src/browserbase/_utils/_utils.py b/src/browserbase/_utils/_utils.py index f0818595..50d59269 100644 --- a/src/browserbase/_utils/_utils.py +++ b/src/browserbase/_utils/_utils.py @@ -21,7 +21,7 @@ import sniffio -from .._types import NotGiven, FileTypes, NotGivenOr, HeadersLike +from .._types import Omit, NotGiven, FileTypes, HeadersLike _T = TypeVar("_T") _TupleT = TypeVar("_TupleT", bound=Tuple[object, ...]) @@ -63,7 +63,7 @@ def _extract_items( try: key = path[index] except IndexError: - if isinstance(obj, NotGiven): + if not is_given(obj): # no value was provided - we can safely ignore return [] @@ -126,8 +126,8 @@ def _extract_items( return [] -def is_given(obj: NotGivenOr[_T]) -> TypeGuard[_T]: - return not isinstance(obj, NotGiven) +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. diff --git a/src/browserbase/resources/contexts.py b/src/browserbase/resources/contexts.py index bc4d1cc8..d2bb4167 100644 --- a/src/browserbase/resources/contexts.py +++ b/src/browserbase/resources/contexts.py @@ -5,7 +5,7 @@ import httpx from ..types import context_create_params -from .._types import NOT_GIVEN, Body, Query, Headers, NotGiven +from .._types import Body, Query, Headers, NotGiven, not_given from .._utils import maybe_transform, async_maybe_transform from .._compat import cached_property from .._resource import SyncAPIResource, AsyncAPIResource @@ -52,7 +52,7 @@ def create( extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> ContextCreateResponse: """Create a Context @@ -88,7 +88,7 @@ def retrieve( extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> ContextRetrieveResponse: """ Get a Context @@ -121,7 +121,7 @@ def update( extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> ContextUpdateResponse: """ Update a Context @@ -175,7 +175,7 @@ async def create( extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> ContextCreateResponse: """Create a Context @@ -211,7 +211,7 @@ async def retrieve( extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> ContextRetrieveResponse: """ Get a Context @@ -244,7 +244,7 @@ async def update( extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> ContextUpdateResponse: """ Update a Context diff --git a/src/browserbase/resources/extensions.py b/src/browserbase/resources/extensions.py index 4dcd248f..21d06e70 100644 --- a/src/browserbase/resources/extensions.py +++ b/src/browserbase/resources/extensions.py @@ -7,7 +7,7 @@ import httpx from ..types import extension_create_params -from .._types import NOT_GIVEN, Body, Query, Headers, NoneType, NotGiven, FileTypes +from .._types import Body, Query, Headers, NoneType, NotGiven, FileTypes, not_given from .._utils import extract_files, maybe_transform, deepcopy_minimal, async_maybe_transform from .._compat import cached_property from .._resource import SyncAPIResource, AsyncAPIResource @@ -53,7 +53,7 @@ def create( extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> ExtensionCreateResponse: """ Upload an Extension @@ -92,7 +92,7 @@ def retrieve( extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> ExtensionRetrieveResponse: """ Get an Extension @@ -125,7 +125,7 @@ def delete( extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> None: """ Delete an Extension @@ -180,7 +180,7 @@ async def create( extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> ExtensionCreateResponse: """ Upload an Extension @@ -219,7 +219,7 @@ async def retrieve( extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> ExtensionRetrieveResponse: """ Get an Extension @@ -252,7 +252,7 @@ async def delete( extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> None: """ Delete an Extension diff --git a/src/browserbase/resources/projects.py b/src/browserbase/resources/projects.py index e0e73b40..62c28afa 100644 --- a/src/browserbase/resources/projects.py +++ b/src/browserbase/resources/projects.py @@ -4,7 +4,7 @@ import httpx -from .._types import NOT_GIVEN, Body, Query, Headers, NotGiven +from .._types import Body, Query, Headers, NotGiven, not_given from .._compat import cached_property from .._resource import SyncAPIResource, AsyncAPIResource from .._response import ( @@ -50,7 +50,7 @@ def retrieve( extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> ProjectRetrieveResponse: """ Get a Project @@ -82,7 +82,7 @@ def list( extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> ProjectListResponse: """List Projects""" return self._get( @@ -102,7 +102,7 @@ def usage( extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> ProjectUsageResponse: """ Get Project Usage @@ -156,7 +156,7 @@ async def retrieve( extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> ProjectRetrieveResponse: """ Get a Project @@ -188,7 +188,7 @@ async def list( extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> ProjectListResponse: """List Projects""" return await self._get( @@ -208,7 +208,7 @@ async def usage( extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> ProjectUsageResponse: """ Get Project Usage diff --git a/src/browserbase/resources/sessions/downloads.py b/src/browserbase/resources/sessions/downloads.py index 9ee49759..6195c30b 100644 --- a/src/browserbase/resources/sessions/downloads.py +++ b/src/browserbase/resources/sessions/downloads.py @@ -4,7 +4,7 @@ import httpx -from ..._types import NOT_GIVEN, Body, Query, Headers, NotGiven +from ..._types import Body, Query, Headers, NotGiven, not_given from ..._compat import cached_property from ..._resource import SyncAPIResource, AsyncAPIResource from ..._response import ( @@ -51,7 +51,7 @@ def list( extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> BinaryAPIResponse: """ Session Downloads @@ -106,7 +106,7 @@ async def list( extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> AsyncBinaryAPIResponse: """ Session Downloads diff --git a/src/browserbase/resources/sessions/logs.py b/src/browserbase/resources/sessions/logs.py index 2a42c9dc..b1c90f52 100644 --- a/src/browserbase/resources/sessions/logs.py +++ b/src/browserbase/resources/sessions/logs.py @@ -4,7 +4,7 @@ import httpx -from ..._types import NOT_GIVEN, Body, Query, Headers, NotGiven +from ..._types import Body, Query, Headers, NotGiven, not_given from ..._compat import cached_property from ..._resource import SyncAPIResource, AsyncAPIResource from ..._response import ( @@ -48,7 +48,7 @@ def list( extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> LogListResponse: """ Session Logs @@ -102,7 +102,7 @@ async def list( extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> LogListResponse: """ Session Logs diff --git a/src/browserbase/resources/sessions/recording.py b/src/browserbase/resources/sessions/recording.py index 856b2927..789087a8 100644 --- a/src/browserbase/resources/sessions/recording.py +++ b/src/browserbase/resources/sessions/recording.py @@ -4,7 +4,7 @@ import httpx -from ..._types import NOT_GIVEN, Body, Query, Headers, NotGiven +from ..._types import Body, Query, Headers, NotGiven, not_given from ..._compat import cached_property from ..._resource import SyncAPIResource, AsyncAPIResource from ..._response import ( @@ -48,7 +48,7 @@ def retrieve( extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> RecordingRetrieveResponse: """ Session Recording @@ -102,7 +102,7 @@ async def retrieve( extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> RecordingRetrieveResponse: """ Session Recording diff --git a/src/browserbase/resources/sessions/sessions.py b/src/browserbase/resources/sessions/sessions.py index 01a4943a..ceaaeb81 100644 --- a/src/browserbase/resources/sessions/sessions.py +++ b/src/browserbase/resources/sessions/sessions.py @@ -24,7 +24,7 @@ UploadsResourceWithStreamingResponse, AsyncUploadsResourceWithStreamingResponse, ) -from ..._types import NOT_GIVEN, Body, Query, Headers, NotGiven +from ..._types import Body, Omit, Query, Headers, NotGiven, omit, not_given from ..._utils import maybe_transform, async_maybe_transform from ..._compat import cached_property from .downloads import ( @@ -100,19 +100,19 @@ def create( self, *, project_id: str, - browser_settings: session_create_params.BrowserSettings | NotGiven = NOT_GIVEN, - extension_id: str | NotGiven = NOT_GIVEN, - keep_alive: bool | NotGiven = NOT_GIVEN, - proxies: Union[Iterable[session_create_params.ProxiesUnionMember0], bool] | NotGiven = NOT_GIVEN, - region: Literal["us-west-2", "us-east-1", "eu-central-1", "ap-southeast-1"] | NotGiven = NOT_GIVEN, - api_timeout: int | NotGiven = NOT_GIVEN, - user_metadata: Dict[str, object] | NotGiven = NOT_GIVEN, + browser_settings: session_create_params.BrowserSettings | Omit = omit, + extension_id: str | Omit = omit, + keep_alive: bool | Omit = omit, + proxies: Union[Iterable[session_create_params.ProxiesUnionMember0], bool] | Omit = omit, + region: Literal["us-west-2", "us-east-1", "eu-central-1", "ap-southeast-1"] | Omit = omit, + api_timeout: int | Omit = omit, + user_metadata: Dict[str, 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, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> SessionCreateResponse: """Create a Session @@ -177,7 +177,7 @@ def retrieve( extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> SessionRetrieveResponse: """ Get a Session @@ -212,7 +212,7 @@ def update( extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> SessionUpdateResponse: """Update a Session @@ -253,14 +253,14 @@ def update( def list( self, *, - q: str | NotGiven = NOT_GIVEN, - status: Literal["RUNNING", "ERROR", "TIMED_OUT", "COMPLETED"] | NotGiven = NOT_GIVEN, + q: str | Omit = omit, + status: Literal["RUNNING", "ERROR", "TIMED_OUT", "COMPLETED"] | 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, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> SessionListResponse: """List Sessions @@ -306,7 +306,7 @@ def debug( extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> SessionDebugResponse: """ Session Live URLs @@ -371,19 +371,19 @@ async def create( self, *, project_id: str, - browser_settings: session_create_params.BrowserSettings | NotGiven = NOT_GIVEN, - extension_id: str | NotGiven = NOT_GIVEN, - keep_alive: bool | NotGiven = NOT_GIVEN, - proxies: Union[Iterable[session_create_params.ProxiesUnionMember0], bool] | NotGiven = NOT_GIVEN, - region: Literal["us-west-2", "us-east-1", "eu-central-1", "ap-southeast-1"] | NotGiven = NOT_GIVEN, - api_timeout: int | NotGiven = NOT_GIVEN, - user_metadata: Dict[str, object] | NotGiven = NOT_GIVEN, + browser_settings: session_create_params.BrowserSettings | Omit = omit, + extension_id: str | Omit = omit, + keep_alive: bool | Omit = omit, + proxies: Union[Iterable[session_create_params.ProxiesUnionMember0], bool] | Omit = omit, + region: Literal["us-west-2", "us-east-1", "eu-central-1", "ap-southeast-1"] | Omit = omit, + api_timeout: int | Omit = omit, + user_metadata: Dict[str, 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, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> SessionCreateResponse: """Create a Session @@ -448,7 +448,7 @@ async def retrieve( extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> SessionRetrieveResponse: """ Get a Session @@ -483,7 +483,7 @@ async def update( extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> SessionUpdateResponse: """Update a Session @@ -524,14 +524,14 @@ async def update( async def list( self, *, - q: str | NotGiven = NOT_GIVEN, - status: Literal["RUNNING", "ERROR", "TIMED_OUT", "COMPLETED"] | NotGiven = NOT_GIVEN, + q: str | Omit = omit, + status: Literal["RUNNING", "ERROR", "TIMED_OUT", "COMPLETED"] | 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, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> SessionListResponse: """List Sessions @@ -577,7 +577,7 @@ async def debug( extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> SessionDebugResponse: """ Session Live URLs diff --git a/src/browserbase/resources/sessions/uploads.py b/src/browserbase/resources/sessions/uploads.py index 69b6ccbe..aba72b64 100644 --- a/src/browserbase/resources/sessions/uploads.py +++ b/src/browserbase/resources/sessions/uploads.py @@ -6,7 +6,7 @@ import httpx -from ..._types import NOT_GIVEN, Body, Query, Headers, NotGiven, FileTypes +from ..._types import Body, Query, Headers, NotGiven, FileTypes, not_given from ..._utils import extract_files, maybe_transform, deepcopy_minimal, async_maybe_transform from ..._compat import cached_property from ..._resource import SyncAPIResource, AsyncAPIResource @@ -53,7 +53,7 @@ def create( extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> UploadCreateResponse: """ Create Session Uploads @@ -116,7 +116,7 @@ async def create( extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> UploadCreateResponse: """ Create Session Uploads diff --git a/tests/test_transform.py b/tests/test_transform.py index 498d0d93..c31b1f40 100644 --- a/tests/test_transform.py +++ b/tests/test_transform.py @@ -8,7 +8,7 @@ import pytest -from browserbase._types import NOT_GIVEN, Base64FileInput +from browserbase._types import Base64FileInput, omit, not_given from browserbase._utils import ( PropertyInfo, transform as _transform, @@ -450,4 +450,11 @@ async def test_transform_skipping(use_async: bool) -> None: @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) == {} + 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) == {} From f09dfcdb7498cd9be3e1b276e13b76942517900c Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 20 Sep 2025 03:38:43 +0000 Subject: [PATCH 205/330] chore: do not install brew dependencies in ./scripts/bootstrap by default --- scripts/bootstrap | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/scripts/bootstrap b/scripts/bootstrap index e84fe62c..b430fee3 100755 --- a/scripts/bootstrap +++ b/scripts/bootstrap @@ -4,10 +4,18 @@ set -e cd "$(dirname "$0")/.." -if ! command -v rye >/dev/null 2>&1 && [ -f "Brewfile" ] && [ "$(uname -s)" = "Darwin" ]; then +if [ -f "Brewfile" ] && [ "$(uname -s)" = "Darwin" ] && [ "$SKIP_BREW" != "1" ] && [ -t 0 ]; then brew bundle check >/dev/null 2>&1 || { - echo "==> Installing Homebrew dependencies…" - brew bundle + echo -n "==> Install Homebrew dependencies? (y/N): " + read -r response + case "$response" in + [yY][eE][sS]|[yY]) + brew bundle + ;; + *) + ;; + esac + echo } fi From eee0eaad46fbd4b2fc29399d319c20b054dafc60 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 7 Oct 2025 08:32:50 +0000 Subject: [PATCH 206/330] feat(api): manual updates --- .stats.yml | 4 ++-- src/browserbase/resources/sessions/sessions.py | 8 ++++++++ src/browserbase/types/session_create_params.py | 9 +++++++++ tests/api_resources/test_sessions.py | 2 ++ 4 files changed, 21 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index b000c8c1..e1cd805d 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 18 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fbrowserbase-be7a4aeebb1605262935b4b3ab446a95b1fad8a7d18098943dd548c8a486ef13.yml -openapi_spec_hash: 1c950a109f80140711e7ae2cf87fddad +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fbrowserbase-0b96e0120f7cf3fba797371433e15a08d14727c0526d718b728faee615624297.yml +openapi_spec_hash: 8d007eed388933bf9d74c5488a56be41 config_hash: b3ca4ec5b02e5333af51ebc2e9fdef1b diff --git a/src/browserbase/resources/sessions/sessions.py b/src/browserbase/resources/sessions/sessions.py index ceaaeb81..5e58bbbb 100644 --- a/src/browserbase/resources/sessions/sessions.py +++ b/src/browserbase/resources/sessions/sessions.py @@ -104,6 +104,7 @@ def create( extension_id: str | Omit = omit, keep_alive: bool | Omit = omit, proxies: Union[Iterable[session_create_params.ProxiesUnionMember0], bool] | Omit = omit, + proxy_settings: session_create_params.ProxySettings | Omit = omit, region: Literal["us-west-2", "us-east-1", "eu-central-1", "ap-southeast-1"] | Omit = omit, api_timeout: int | Omit = omit, user_metadata: Dict[str, object] | Omit = omit, @@ -131,6 +132,8 @@ def create( proxies: Proxy configuration. Can be true for default proxy, or an array of proxy configurations. + proxy_settings: [NOT IN DOCS] Supplementary proxy settings. Optional. + region: The region where the Session should run. api_timeout: Duration in seconds after which the session will automatically end. Defaults to @@ -156,6 +159,7 @@ def create( "extension_id": extension_id, "keep_alive": keep_alive, "proxies": proxies, + "proxy_settings": proxy_settings, "region": region, "api_timeout": api_timeout, "user_metadata": user_metadata, @@ -375,6 +379,7 @@ async def create( extension_id: str | Omit = omit, keep_alive: bool | Omit = omit, proxies: Union[Iterable[session_create_params.ProxiesUnionMember0], bool] | Omit = omit, + proxy_settings: session_create_params.ProxySettings | Omit = omit, region: Literal["us-west-2", "us-east-1", "eu-central-1", "ap-southeast-1"] | Omit = omit, api_timeout: int | Omit = omit, user_metadata: Dict[str, object] | Omit = omit, @@ -402,6 +407,8 @@ async def create( proxies: Proxy configuration. Can be true for default proxy, or an array of proxy configurations. + proxy_settings: [NOT IN DOCS] Supplementary proxy settings. Optional. + region: The region where the Session should run. api_timeout: Duration in seconds after which the session will automatically end. Defaults to @@ -427,6 +434,7 @@ async def create( "extension_id": extension_id, "keep_alive": keep_alive, "proxies": proxies, + "proxy_settings": proxy_settings, "region": region, "api_timeout": api_timeout, "user_metadata": user_metadata, diff --git a/src/browserbase/types/session_create_params.py b/src/browserbase/types/session_create_params.py index 3a517c06..7fafe448 100644 --- a/src/browserbase/types/session_create_params.py +++ b/src/browserbase/types/session_create_params.py @@ -19,6 +19,7 @@ "ProxiesUnionMember0UnionMember0", "ProxiesUnionMember0UnionMember0Geolocation", "ProxiesUnionMember0UnionMember1", + "ProxySettings", ] @@ -49,6 +50,9 @@ class SessionCreateParams(TypedDict, total=False): Can be true for default proxy, or an array of proxy configurations. """ + proxy_settings: Annotated[ProxySettings, PropertyInfo(alias="proxySettings")] + """[NOT IN DOCS] Supplementary proxy settings. Optional.""" + region: Literal["us-west-2", "us-east-1", "eu-central-1", "ap-southeast-1"] """The region where the Session should run.""" @@ -208,3 +212,8 @@ class ProxiesUnionMember0UnionMember1(TypedDict, total=False): ProxiesUnionMember0: TypeAlias = Union[ProxiesUnionMember0UnionMember0, ProxiesUnionMember0UnionMember1] + + +class ProxySettings(TypedDict, total=False): + ca_certificates: Required[Annotated[SequenceNotStr[str], PropertyInfo(alias="caCertificates")]] + """[NOT IN DOCS] The TLS certificate IDs to trust. Optional.""" diff --git a/tests/api_resources/test_sessions.py b/tests/api_resources/test_sessions.py index 7a16f64f..24da8f0b 100644 --- a/tests/api_resources/test_sessions.py +++ b/tests/api_resources/test_sessions.py @@ -79,6 +79,7 @@ def test_method_create_with_all_params(self, client: Browserbase) -> None: }, } ], + proxy_settings={"ca_certificates": ["182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e"]}, region="us-west-2", api_timeout=60, user_metadata={"foo": "bar"}, @@ -326,6 +327,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncBrowserbas }, } ], + proxy_settings={"ca_certificates": ["182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e"]}, region="us-west-2", api_timeout=60, user_metadata={"foo": "bar"}, From 91299342b1988334e8689b35257d9db0ae46c7dd Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 7 Oct 2025 08:38:19 +0000 Subject: [PATCH 207/330] chore(internal): version bump --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/browserbase/_version.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 7a22c4a0..3d362d5e 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "1.5.0-alpha.0" + ".": "1.5.0-alpha.1" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 9d142ec4..2587aaf2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "browserbase" -version = "1.5.0-alpha.0" +version = "1.5.0-alpha.1" description = "The official Python library for the Browserbase API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/browserbase/_version.py b/src/browserbase/_version.py index 25f19565..6fa8f70b 100644 --- a/src/browserbase/_version.py +++ b/src/browserbase/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "browserbase" -__version__ = "1.5.0-alpha.0" # x-release-please-version +__version__ = "1.5.0-alpha.1" # x-release-please-version From e75afcc9cf296d24049474958bd4bf22cc310956 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 7 Oct 2025 14:49:00 +0000 Subject: [PATCH 208/330] feat(api): api update --- .stats.yml | 4 ++-- src/browserbase/resources/sessions/sessions.py | 8 -------- src/browserbase/types/session_create_params.py | 9 --------- tests/api_resources/test_sessions.py | 2 -- 4 files changed, 2 insertions(+), 21 deletions(-) diff --git a/.stats.yml b/.stats.yml index e1cd805d..b000c8c1 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 18 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fbrowserbase-0b96e0120f7cf3fba797371433e15a08d14727c0526d718b728faee615624297.yml -openapi_spec_hash: 8d007eed388933bf9d74c5488a56be41 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fbrowserbase-be7a4aeebb1605262935b4b3ab446a95b1fad8a7d18098943dd548c8a486ef13.yml +openapi_spec_hash: 1c950a109f80140711e7ae2cf87fddad config_hash: b3ca4ec5b02e5333af51ebc2e9fdef1b diff --git a/src/browserbase/resources/sessions/sessions.py b/src/browserbase/resources/sessions/sessions.py index 5e58bbbb..ceaaeb81 100644 --- a/src/browserbase/resources/sessions/sessions.py +++ b/src/browserbase/resources/sessions/sessions.py @@ -104,7 +104,6 @@ def create( extension_id: str | Omit = omit, keep_alive: bool | Omit = omit, proxies: Union[Iterable[session_create_params.ProxiesUnionMember0], bool] | Omit = omit, - proxy_settings: session_create_params.ProxySettings | Omit = omit, region: Literal["us-west-2", "us-east-1", "eu-central-1", "ap-southeast-1"] | Omit = omit, api_timeout: int | Omit = omit, user_metadata: Dict[str, object] | Omit = omit, @@ -132,8 +131,6 @@ def create( proxies: Proxy configuration. Can be true for default proxy, or an array of proxy configurations. - proxy_settings: [NOT IN DOCS] Supplementary proxy settings. Optional. - region: The region where the Session should run. api_timeout: Duration in seconds after which the session will automatically end. Defaults to @@ -159,7 +156,6 @@ def create( "extension_id": extension_id, "keep_alive": keep_alive, "proxies": proxies, - "proxy_settings": proxy_settings, "region": region, "api_timeout": api_timeout, "user_metadata": user_metadata, @@ -379,7 +375,6 @@ async def create( extension_id: str | Omit = omit, keep_alive: bool | Omit = omit, proxies: Union[Iterable[session_create_params.ProxiesUnionMember0], bool] | Omit = omit, - proxy_settings: session_create_params.ProxySettings | Omit = omit, region: Literal["us-west-2", "us-east-1", "eu-central-1", "ap-southeast-1"] | Omit = omit, api_timeout: int | Omit = omit, user_metadata: Dict[str, object] | Omit = omit, @@ -407,8 +402,6 @@ async def create( proxies: Proxy configuration. Can be true for default proxy, or an array of proxy configurations. - proxy_settings: [NOT IN DOCS] Supplementary proxy settings. Optional. - region: The region where the Session should run. api_timeout: Duration in seconds after which the session will automatically end. Defaults to @@ -434,7 +427,6 @@ async def create( "extension_id": extension_id, "keep_alive": keep_alive, "proxies": proxies, - "proxy_settings": proxy_settings, "region": region, "api_timeout": api_timeout, "user_metadata": user_metadata, diff --git a/src/browserbase/types/session_create_params.py b/src/browserbase/types/session_create_params.py index 7fafe448..3a517c06 100644 --- a/src/browserbase/types/session_create_params.py +++ b/src/browserbase/types/session_create_params.py @@ -19,7 +19,6 @@ "ProxiesUnionMember0UnionMember0", "ProxiesUnionMember0UnionMember0Geolocation", "ProxiesUnionMember0UnionMember1", - "ProxySettings", ] @@ -50,9 +49,6 @@ class SessionCreateParams(TypedDict, total=False): Can be true for default proxy, or an array of proxy configurations. """ - proxy_settings: Annotated[ProxySettings, PropertyInfo(alias="proxySettings")] - """[NOT IN DOCS] Supplementary proxy settings. Optional.""" - region: Literal["us-west-2", "us-east-1", "eu-central-1", "ap-southeast-1"] """The region where the Session should run.""" @@ -212,8 +208,3 @@ class ProxiesUnionMember0UnionMember1(TypedDict, total=False): ProxiesUnionMember0: TypeAlias = Union[ProxiesUnionMember0UnionMember0, ProxiesUnionMember0UnionMember1] - - -class ProxySettings(TypedDict, total=False): - ca_certificates: Required[Annotated[SequenceNotStr[str], PropertyInfo(alias="caCertificates")]] - """[NOT IN DOCS] The TLS certificate IDs to trust. Optional.""" diff --git a/tests/api_resources/test_sessions.py b/tests/api_resources/test_sessions.py index 24da8f0b..7a16f64f 100644 --- a/tests/api_resources/test_sessions.py +++ b/tests/api_resources/test_sessions.py @@ -79,7 +79,6 @@ def test_method_create_with_all_params(self, client: Browserbase) -> None: }, } ], - proxy_settings={"ca_certificates": ["182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e"]}, region="us-west-2", api_timeout=60, user_metadata={"foo": "bar"}, @@ -327,7 +326,6 @@ async def test_method_create_with_all_params(self, async_client: AsyncBrowserbas }, } ], - proxy_settings={"ca_certificates": ["182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e"]}, region="us-west-2", api_timeout=60, user_metadata={"foo": "bar"}, From 636e9a1a80fa17e34e40fa81881dc8beef6dfab2 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 11 Oct 2025 02:36:32 +0000 Subject: [PATCH 209/330] chore(internal): detect missing future annotations with ruff --- pyproject.toml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 2587aaf2..52bb5230 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -224,6 +224,8 @@ select = [ "B", # remove unused imports "F401", + # check for missing future annotations + "FA102", # bare except statements "E722", # unused arguments @@ -246,6 +248,8 @@ unfixable = [ "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" From 9b169e478fae25d770bfe0a3f829a1eee39104a7 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 18 Oct 2025 02:22:48 +0000 Subject: [PATCH 210/330] chore: bump `httpx-aiohttp` version to 0.1.9 --- pyproject.toml | 2 +- requirements-dev.lock | 2 +- requirements.lock | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 52bb5230..e39b76a7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,7 +39,7 @@ Homepage = "https://github.com/browserbase/sdk-python" Repository = "https://github.com/browserbase/sdk-python" [project.optional-dependencies] -aiohttp = ["aiohttp", "httpx_aiohttp>=0.1.8"] +aiohttp = ["aiohttp", "httpx_aiohttp>=0.1.9"] [tool.rye] managed = true diff --git a/requirements-dev.lock b/requirements-dev.lock index 7045395d..64352852 100644 --- a/requirements-dev.lock +++ b/requirements-dev.lock @@ -56,7 +56,7 @@ httpx==0.28.1 # via browserbase # via httpx-aiohttp # via respx -httpx-aiohttp==0.1.8 +httpx-aiohttp==0.1.9 # via browserbase idna==3.4 # via anyio diff --git a/requirements.lock b/requirements.lock index 2495a260..55ea8833 100644 --- a/requirements.lock +++ b/requirements.lock @@ -43,7 +43,7 @@ httpcore==1.0.9 httpx==0.28.1 # via browserbase # via httpx-aiohttp -httpx-aiohttp==0.1.8 +httpx-aiohttp==0.1.9 # via browserbase idna==3.4 # via anyio From e8bc81c110f2c4210dd0471ea7c82521e6be0a8d Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 30 Oct 2025 02:46:27 +0000 Subject: [PATCH 211/330] fix(client): close streams without requiring full consumption --- src/browserbase/_streaming.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/browserbase/_streaming.py b/src/browserbase/_streaming.py index c04b2332..129714aa 100644 --- a/src/browserbase/_streaming.py +++ b/src/browserbase/_streaming.py @@ -57,9 +57,8 @@ def __stream__(self) -> Iterator[_T]: for sse in iterator: yield process_data(data=sse.json(), cast_to=cast_to, response=response) - # Ensure the entire stream is consumed - for _sse in iterator: - ... + # As we might not fully consume the response stream, we need to close it explicitly + response.close() def __enter__(self) -> Self: return self @@ -121,9 +120,8 @@ async def __stream__(self) -> AsyncIterator[_T]: async for sse in iterator: yield process_data(data=sse.json(), cast_to=cast_to, response=response) - # Ensure the entire stream is consumed - async for _sse in iterator: - ... + # As we might not fully consume the response stream, we need to close it explicitly + await response.aclose() async def __aenter__(self) -> Self: return self From c669aecf20f23c526054c3ec291e4890af8b31db Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 31 Oct 2025 03:57:17 +0000 Subject: [PATCH 212/330] chore(internal/tests): avoid race condition with implicit client cleanup --- tests/test_client.py | 364 ++++++++++++++++++++++++------------------- 1 file changed, 200 insertions(+), 164 deletions(-) diff --git a/tests/test_client.py b/tests/test_client.py index aed68baf..db556fc2 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -59,51 +59,49 @@ def _get_open_connections(client: Browserbase | AsyncBrowserbase) -> int: class TestBrowserbase: - client = Browserbase(base_url=base_url, api_key=api_key, _strict_response_validation=True) - @pytest.mark.respx(base_url=base_url) - def test_raw_response(self, respx_mock: MockRouter) -> 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 = self.client.post("/foo", cast_to=httpx.Response) + 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) -> 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"}') ) - response = self.client.post("/foo", cast_to=httpx.Response) + 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) -> None: - copied = self.client.copy() - assert id(copied) != id(self.client) + def test_copy(self, client: Browserbase) -> None: + copied = client.copy() + assert id(copied) != id(client) - copied = self.client.copy(api_key="another My API Key") + copied = client.copy(api_key="another My API Key") assert copied.api_key == "another My API Key" - assert self.client.api_key == "My API Key" + assert client.api_key == "My API Key" - def test_copy_default_options(self) -> None: + def test_copy_default_options(self, client: Browserbase) -> None: # options that have a default are overridden correctly - copied = self.client.copy(max_retries=7) + copied = client.copy(max_retries=7) assert copied.max_retries == 7 - assert self.client.max_retries == 2 + assert client.max_retries == 2 copied2 = copied.copy(max_retries=6) assert copied2.max_retries == 6 assert copied.max_retries == 7 # timeout - assert isinstance(self.client.timeout, httpx.Timeout) - copied = self.client.copy(timeout=None) + assert isinstance(client.timeout, httpx.Timeout) + copied = client.copy(timeout=None) assert copied.timeout is None - assert isinstance(self.client.timeout, httpx.Timeout) + assert isinstance(client.timeout, httpx.Timeout) def test_copy_default_headers(self) -> None: client = Browserbase( @@ -138,6 +136,7 @@ def test_copy_default_headers(self) -> None: 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 = Browserbase( @@ -175,13 +174,15 @@ def test_copy_default_query(self) -> None: ): client.copy(set_default_query={}, default_query={"foo": "Bar"}) - def test_copy_signature(self) -> None: + client.close() + + 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. - self.client.__init__, # type: ignore[misc] + client.__init__, # type: ignore[misc] ) - copy_signature = inspect.signature(self.client.copy) + copy_signature = inspect.signature(client.copy) exclude_params = {"transport", "proxies", "_strict_response_validation"} for name in init_signature.parameters.keys(): @@ -192,12 +193,12 @@ def test_copy_signature(self) -> 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) -> None: + def test_copy_build_request(self, client: Browserbase) -> None: options = FinalRequestOptions(method="get", url="/foo") def build_request(options: FinalRequestOptions) -> None: - client = self.client.copy() - client._build_request(options) + client_copy = client.copy() + client_copy._build_request(options) # ensure that the machinery is warmed up before tracing starts. build_request(options) @@ -254,14 +255,12 @@ def add_leak(leaks: list[tracemalloc.StatisticDiff], diff: tracemalloc.Statistic print(frame) raise AssertionError() - def test_request_timeout(self) -> None: - request = self.client._build_request(FinalRequestOptions(method="get", url="/foo")) + 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 - request = self.client._build_request( - FinalRequestOptions(method="get", url="/foo", timeout=httpx.Timeout(100.0)) - ) + 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) @@ -274,6 +273,8 @@ def test_client_timeout_option(self) -> None: 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: @@ -285,6 +286,8 @@ def test_http_client_timeout_option(self) -> None: 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 = Browserbase( @@ -295,6 +298,8 @@ def test_http_client_timeout_option(self) -> None: 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 = Browserbase( @@ -305,6 +310,8 @@ def test_http_client_timeout_option(self) -> None: 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: @@ -316,14 +323,14 @@ async def test_invalid_http_client(self) -> None: ) def test_default_headers_option(self) -> None: - client = Browserbase( + test_client = Browserbase( base_url=base_url, api_key=api_key, _strict_response_validation=True, default_headers={"X-Foo": "bar"} ) - request = client._build_request(FinalRequestOptions(method="get", url="/foo")) + 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" - client2 = Browserbase( + test_client2 = Browserbase( base_url=base_url, api_key=api_key, _strict_response_validation=True, @@ -332,10 +339,13 @@ def test_default_headers_option(self) -> None: "X-Stainless-Lang": "my-overriding-header", }, ) - request = client2._build_request(FinalRequestOptions(method="get", url="/foo")) + 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 = Browserbase(base_url=base_url, api_key=api_key, _strict_response_validation=True) request = client._build_request(FinalRequestOptions(method="get", url="/foo")) @@ -364,8 +374,10 @@ def test_default_query_option(self) -> None: url = httpx.URL(request.url) assert dict(url.params) == {"foo": "baz", "query_param": "overridden"} - def test_request_extra_json(self) -> None: - request = self.client._build_request( + client.close() + + def test_request_extra_json(self, client: Browserbase) -> None: + request = client._build_request( FinalRequestOptions( method="post", url="/foo", @@ -376,7 +388,7 @@ def test_request_extra_json(self) -> None: data = json.loads(request.content.decode("utf-8")) assert data == {"foo": "bar", "baz": False} - request = self.client._build_request( + request = client._build_request( FinalRequestOptions( method="post", url="/foo", @@ -387,7 +399,7 @@ def test_request_extra_json(self) -> None: assert data == {"baz": False} # `extra_json` takes priority over `json_data` when keys clash - request = self.client._build_request( + request = client._build_request( FinalRequestOptions( method="post", url="/foo", @@ -398,8 +410,8 @@ def test_request_extra_json(self) -> None: data = json.loads(request.content.decode("utf-8")) assert data == {"foo": "bar", "baz": None} - def test_request_extra_headers(self) -> None: - request = self.client._build_request( + def test_request_extra_headers(self, client: Browserbase) -> None: + request = client._build_request( FinalRequestOptions( method="post", url="/foo", @@ -409,7 +421,7 @@ def test_request_extra_headers(self) -> None: assert request.headers.get("X-Foo") == "Foo" # `extra_headers` takes priority over `default_headers` when keys clash - request = self.client.with_options(default_headers={"X-Bar": "true"})._build_request( + request = client.with_options(default_headers={"X-Bar": "true"})._build_request( FinalRequestOptions( method="post", url="/foo", @@ -420,8 +432,8 @@ def test_request_extra_headers(self) -> None: ) assert request.headers.get("X-Bar") == "false" - def test_request_extra_query(self) -> None: - request = self.client._build_request( + def test_request_extra_query(self, client: Browserbase) -> None: + request = client._build_request( FinalRequestOptions( method="post", url="/foo", @@ -434,7 +446,7 @@ def test_request_extra_query(self) -> None: assert params == {"my_query_param": "Foo"} # if both `query` and `extra_query` are given, they are merged - request = self.client._build_request( + request = client._build_request( FinalRequestOptions( method="post", url="/foo", @@ -448,7 +460,7 @@ def test_request_extra_query(self) -> None: assert params == {"bar": "1", "foo": "2"} # `extra_query` takes priority over `query` when keys clash - request = self.client._build_request( + request = client._build_request( FinalRequestOptions( method="post", url="/foo", @@ -491,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) -> None: + def test_basic_union_response(self, respx_mock: MockRouter, client: Browserbase) -> None: class Model1(BaseModel): name: str @@ -500,12 +512,12 @@ class Model2(BaseModel): respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"})) - response = self.client.get("/foo", cast_to=cast(Any, Union[Model1, Model2])) + 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) -> 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): @@ -516,18 +528,18 @@ class Model2(BaseModel): respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"})) - response = self.client.get("/foo", cast_to=cast(Any, Union[Model1, Model2])) + 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 = self.client.get("/foo", cast_to=cast(Any, Union[Model1, Model2])) + 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) -> 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 """ @@ -543,7 +555,7 @@ class Model(BaseModel): ) ) - response = self.client.get("/foo", cast_to=Model) + response = client.get("/foo", cast_to=Model) assert isinstance(response, Model) assert response.foo == 2 @@ -557,6 +569,8 @@ def test_base_url_setter(self) -> None: assert client.base_url == "https://example.com/from_setter/" + 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) @@ -586,6 +600,7 @@ def test_base_url_trailing_slash(self, client: Browserbase) -> None: ), ) assert request.url == "http://localhost:5000/custom/path/foo" + client.close() @pytest.mark.parametrize( "client", @@ -611,6 +626,7 @@ def test_base_url_no_trailing_slash(self, client: Browserbase) -> None: ), ) assert request.url == "http://localhost:5000/custom/path/foo" + client.close() @pytest.mark.parametrize( "client", @@ -636,35 +652,36 @@ def test_absolute_request_url(self, client: Browserbase) -> None: ), ) assert request.url == "https://myapi.com/foo" + client.close() def test_copied_client_does_not_close_http(self) -> None: - client = Browserbase(base_url=base_url, api_key=api_key, _strict_response_validation=True) - assert not client.is_closed() + test_client = Browserbase(base_url=base_url, api_key=api_key, _strict_response_validation=True) + assert not test_client.is_closed() - copied = client.copy() - assert copied is not client + copied = test_client.copy() + assert copied is not test_client del copied - assert not client.is_closed() + assert not test_client.is_closed() def test_client_context_manager(self) -> None: - client = Browserbase(base_url=base_url, api_key=api_key, _strict_response_validation=True) - with client as c2: - assert c2 is client + 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() - assert not client.is_closed() - assert client.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) -> None: + def test_client_response_validation_error(self, respx_mock: MockRouter, client: Browserbase) -> 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: - self.client.get("/foo", cast_to=Model) + client.get("/foo", cast_to=Model) assert isinstance(exc.value.__cause__, ValidationError) @@ -686,11 +703,14 @@ class Model(BaseModel): with pytest.raises(APIResponseValidationError): strict_client.get("/foo", cast_to=Model) - client = Browserbase(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 = client.get("/foo", cast_to=Model) + 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", [ @@ -713,9 +733,9 @@ 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) -> None: - client = Browserbase(base_url=base_url, api_key=api_key, _strict_response_validation=True) - + def test_parse_retry_after_header( + 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) calculated = client._calculate_retry_timeout(remaining_retries, options, headers) @@ -729,7 +749,7 @@ def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter, clien with pytest.raises(APITimeoutError): client.sessions.with_streaming_response.create(project_id="projectId").__enter__() - assert _get_open_connections(self.client) == 0 + assert _get_open_connections(client) == 0 @mock.patch("browserbase._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) @pytest.mark.respx(base_url=base_url) @@ -738,7 +758,7 @@ def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter, client with pytest.raises(APIStatusError): client.sessions.with_streaming_response.create(project_id="projectId").__enter__() - assert _get_open_connections(self.client) == 0 + assert _get_open_connections(client) == 0 @pytest.mark.parametrize("failures_before_success", [0, 2, 4]) @mock.patch("browserbase._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) @@ -844,83 +864,77 @@ def test_default_client_creation(self) -> None: ) @pytest.mark.respx(base_url=base_url) - def test_follow_redirects(self, respx_mock: MockRouter) -> 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"}) ) respx_mock.get("/redirected").mock(return_value=httpx.Response(200, json={"status": "ok"})) - response = self.client.post("/redirect", body={"key": "value"}, cast_to=httpx.Response) + 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) -> 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"}) ) with pytest.raises(APIStatusError) as exc_info: - self.client.post( - "/redirect", body={"key": "value"}, options={"follow_redirects": False}, cast_to=httpx.Response - ) + 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 TestAsyncBrowserbase: - client = AsyncBrowserbase(base_url=base_url, api_key=api_key, _strict_response_validation=True) - @pytest.mark.respx(base_url=base_url) - @pytest.mark.asyncio - async def test_raw_response(self, respx_mock: MockRouter) -> 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 self.client.post("/foo", cast_to=httpx.Response) + 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) - @pytest.mark.asyncio - async def test_raw_response_for_binary(self, respx_mock: MockRouter) -> 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"}') ) - response = await self.client.post("/foo", cast_to=httpx.Response) + 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) -> None: - copied = self.client.copy() - assert id(copied) != id(self.client) + def test_copy(self, async_client: AsyncBrowserbase) -> None: + copied = async_client.copy() + assert id(copied) != id(async_client) - copied = self.client.copy(api_key="another My API Key") + copied = async_client.copy(api_key="another My API Key") assert copied.api_key == "another My API Key" - assert self.client.api_key == "My API Key" + assert async_client.api_key == "My API Key" - def test_copy_default_options(self) -> None: + def test_copy_default_options(self, async_client: AsyncBrowserbase) -> None: # options that have a default are overridden correctly - copied = self.client.copy(max_retries=7) + copied = async_client.copy(max_retries=7) assert copied.max_retries == 7 - assert self.client.max_retries == 2 + 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(self.client.timeout, httpx.Timeout) - copied = self.client.copy(timeout=None) + assert isinstance(async_client.timeout, httpx.Timeout) + copied = async_client.copy(timeout=None) assert copied.timeout is None - assert isinstance(self.client.timeout, httpx.Timeout) + assert isinstance(async_client.timeout, httpx.Timeout) - def test_copy_default_headers(self) -> None: + async def test_copy_default_headers(self) -> None: client = AsyncBrowserbase( base_url=base_url, api_key=api_key, _strict_response_validation=True, default_headers={"X-Foo": "bar"} ) @@ -953,8 +967,9 @@ def test_copy_default_headers(self) -> None: match="`default_headers` and `set_default_headers` arguments are mutually exclusive", ): client.copy(set_default_headers={}, default_headers={"X-Foo": "Bar"}) + await client.close() - def test_copy_default_query(self) -> None: + async def test_copy_default_query(self) -> None: client = AsyncBrowserbase( base_url=base_url, api_key=api_key, _strict_response_validation=True, default_query={"foo": "bar"} ) @@ -990,13 +1005,15 @@ def test_copy_default_query(self) -> None: ): client.copy(set_default_query={}, default_query={"foo": "Bar"}) - def test_copy_signature(self) -> None: + await client.close() + + 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. - self.client.__init__, # type: ignore[misc] + async_client.__init__, # type: ignore[misc] ) - copy_signature = inspect.signature(self.client.copy) + copy_signature = inspect.signature(async_client.copy) exclude_params = {"transport", "proxies", "_strict_response_validation"} for name in init_signature.parameters.keys(): @@ -1007,12 +1024,12 @@ def test_copy_signature(self) -> 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) -> None: + def test_copy_build_request(self, async_client: AsyncBrowserbase) -> None: options = FinalRequestOptions(method="get", url="/foo") def build_request(options: FinalRequestOptions) -> None: - client = self.client.copy() - client._build_request(options) + client_copy = async_client.copy() + client_copy._build_request(options) # ensure that the machinery is warmed up before tracing starts. build_request(options) @@ -1069,12 +1086,12 @@ def add_leak(leaks: list[tracemalloc.StatisticDiff], diff: tracemalloc.Statistic print(frame) raise AssertionError() - async def test_request_timeout(self) -> None: - request = self.client._build_request(FinalRequestOptions(method="get", url="/foo")) + 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 - request = self.client._build_request( + request = async_client._build_request( FinalRequestOptions(method="get", url="/foo", timeout=httpx.Timeout(100.0)) ) timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore @@ -1089,6 +1106,8 @@ async def test_client_timeout_option(self) -> None: 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: @@ -1100,6 +1119,8 @@ async def test_http_client_timeout_option(self) -> None: 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 = AsyncBrowserbase( @@ -1110,6 +1131,8 @@ async def test_http_client_timeout_option(self) -> None: 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 = AsyncBrowserbase( @@ -1120,6 +1143,8 @@ async def test_http_client_timeout_option(self) -> None: 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: @@ -1130,15 +1155,15 @@ def test_invalid_http_client(self) -> None: http_client=cast(Any, http_client), ) - def test_default_headers_option(self) -> None: - client = AsyncBrowserbase( + async def test_default_headers_option(self) -> None: + test_client = AsyncBrowserbase( base_url=base_url, api_key=api_key, _strict_response_validation=True, default_headers={"X-Foo": "bar"} ) - request = client._build_request(FinalRequestOptions(method="get", url="/foo")) + 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" - client2 = AsyncBrowserbase( + test_client2 = AsyncBrowserbase( base_url=base_url, api_key=api_key, _strict_response_validation=True, @@ -1147,10 +1172,13 @@ def test_default_headers_option(self) -> None: "X-Stainless-Lang": "my-overriding-header", }, ) - request = client2._build_request(FinalRequestOptions(method="get", url="/foo")) + 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 = AsyncBrowserbase(base_url=base_url, api_key=api_key, _strict_response_validation=True) request = client._build_request(FinalRequestOptions(method="get", url="/foo")) @@ -1161,7 +1189,7 @@ def test_validate_headers(self) -> None: client2 = AsyncBrowserbase(base_url=base_url, api_key=None, _strict_response_validation=True) _ = client2 - def test_default_query_option(self) -> None: + async def test_default_query_option(self) -> None: client = AsyncBrowserbase( base_url=base_url, api_key=api_key, _strict_response_validation=True, default_query={"query_param": "bar"} ) @@ -1179,8 +1207,10 @@ def test_default_query_option(self) -> None: url = httpx.URL(request.url) assert dict(url.params) == {"foo": "baz", "query_param": "overridden"} - def test_request_extra_json(self) -> None: - request = self.client._build_request( + await client.close() + + def test_request_extra_json(self, client: Browserbase) -> None: + request = client._build_request( FinalRequestOptions( method="post", url="/foo", @@ -1191,7 +1221,7 @@ def test_request_extra_json(self) -> None: data = json.loads(request.content.decode("utf-8")) assert data == {"foo": "bar", "baz": False} - request = self.client._build_request( + request = client._build_request( FinalRequestOptions( method="post", url="/foo", @@ -1202,7 +1232,7 @@ def test_request_extra_json(self) -> None: assert data == {"baz": False} # `extra_json` takes priority over `json_data` when keys clash - request = self.client._build_request( + request = client._build_request( FinalRequestOptions( method="post", url="/foo", @@ -1213,8 +1243,8 @@ def test_request_extra_json(self) -> None: data = json.loads(request.content.decode("utf-8")) assert data == {"foo": "bar", "baz": None} - def test_request_extra_headers(self) -> None: - request = self.client._build_request( + def test_request_extra_headers(self, client: Browserbase) -> None: + request = client._build_request( FinalRequestOptions( method="post", url="/foo", @@ -1224,7 +1254,7 @@ def test_request_extra_headers(self) -> None: assert request.headers.get("X-Foo") == "Foo" # `extra_headers` takes priority over `default_headers` when keys clash - request = self.client.with_options(default_headers={"X-Bar": "true"})._build_request( + request = client.with_options(default_headers={"X-Bar": "true"})._build_request( FinalRequestOptions( method="post", url="/foo", @@ -1235,8 +1265,8 @@ def test_request_extra_headers(self) -> None: ) assert request.headers.get("X-Bar") == "false" - def test_request_extra_query(self) -> None: - request = self.client._build_request( + def test_request_extra_query(self, client: Browserbase) -> None: + request = client._build_request( FinalRequestOptions( method="post", url="/foo", @@ -1249,7 +1279,7 @@ def test_request_extra_query(self) -> None: assert params == {"my_query_param": "Foo"} # if both `query` and `extra_query` are given, they are merged - request = self.client._build_request( + request = client._build_request( FinalRequestOptions( method="post", url="/foo", @@ -1263,7 +1293,7 @@ def test_request_extra_query(self) -> None: assert params == {"bar": "1", "foo": "2"} # `extra_query` takes priority over `query` when keys clash - request = self.client._build_request( + request = client._build_request( FinalRequestOptions( method="post", url="/foo", @@ -1306,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) -> None: + async def test_basic_union_response(self, respx_mock: MockRouter, async_client: AsyncBrowserbase) -> None: class Model1(BaseModel): name: str @@ -1315,12 +1345,12 @@ class Model2(BaseModel): respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"})) - response = await self.client.get("/foo", cast_to=cast(Any, Union[Model1, Model2])) + 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) -> 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): @@ -1331,18 +1361,20 @@ class Model2(BaseModel): respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"})) - response = await self.client.get("/foo", cast_to=cast(Any, Union[Model1, Model2])) + 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 self.client.get("/foo", cast_to=cast(Any, Union[Model1, Model2])) + 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) -> None: + async def test_non_application_json_content_type_for_json_data( + self, respx_mock: MockRouter, async_client: AsyncBrowserbase + ) -> None: """ Response that sets Content-Type to something other than application/json but returns json data """ @@ -1358,11 +1390,11 @@ class Model(BaseModel): ) ) - response = await self.client.get("/foo", cast_to=Model) + response = await async_client.get("/foo", cast_to=Model) assert isinstance(response, Model) assert response.foo == 2 - def test_base_url_setter(self) -> None: + async def test_base_url_setter(self) -> None: client = AsyncBrowserbase( base_url="https://example.com/from_init", api_key=api_key, _strict_response_validation=True ) @@ -1372,7 +1404,9 @@ def test_base_url_setter(self) -> None: assert client.base_url == "https://example.com/from_setter/" - def test_base_url_env(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) assert client.base_url == "http://localhost:5000/from/env/" @@ -1392,7 +1426,7 @@ def test_base_url_env(self) -> None: ], ids=["standard", "custom http client"], ) - def test_base_url_trailing_slash(self, client: AsyncBrowserbase) -> None: + async def test_base_url_trailing_slash(self, client: AsyncBrowserbase) -> None: request = client._build_request( FinalRequestOptions( method="post", @@ -1401,6 +1435,7 @@ def test_base_url_trailing_slash(self, client: AsyncBrowserbase) -> None: ), ) assert request.url == "http://localhost:5000/custom/path/foo" + await client.close() @pytest.mark.parametrize( "client", @@ -1417,7 +1452,7 @@ def test_base_url_trailing_slash(self, client: AsyncBrowserbase) -> None: ], ids=["standard", "custom http client"], ) - def test_base_url_no_trailing_slash(self, client: AsyncBrowserbase) -> None: + async def test_base_url_no_trailing_slash(self, client: AsyncBrowserbase) -> None: request = client._build_request( FinalRequestOptions( method="post", @@ -1426,6 +1461,7 @@ def test_base_url_no_trailing_slash(self, client: AsyncBrowserbase) -> None: ), ) assert request.url == "http://localhost:5000/custom/path/foo" + await client.close() @pytest.mark.parametrize( "client", @@ -1442,7 +1478,7 @@ def test_base_url_no_trailing_slash(self, client: AsyncBrowserbase) -> None: ], ids=["standard", "custom http client"], ) - def test_absolute_request_url(self, client: AsyncBrowserbase) -> None: + async def test_absolute_request_url(self, client: AsyncBrowserbase) -> None: request = client._build_request( FinalRequestOptions( method="post", @@ -1451,37 +1487,39 @@ def test_absolute_request_url(self, client: AsyncBrowserbase) -> None: ), ) assert request.url == "https://myapi.com/foo" + await client.close() async def test_copied_client_does_not_close_http(self) -> None: - client = AsyncBrowserbase(base_url=base_url, api_key=api_key, _strict_response_validation=True) - assert not client.is_closed() + test_client = AsyncBrowserbase(base_url=base_url, api_key=api_key, _strict_response_validation=True) + assert not test_client.is_closed() - copied = client.copy() - assert copied is not client + copied = test_client.copy() + assert copied is not test_client del copied await asyncio.sleep(0.2) - assert not client.is_closed() + assert not test_client.is_closed() async def test_client_context_manager(self) -> None: - client = AsyncBrowserbase(base_url=base_url, api_key=api_key, _strict_response_validation=True) - async with client as c2: - assert c2 is client + 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() - assert not client.is_closed() - assert client.is_closed() + assert not test_client.is_closed() + assert test_client.is_closed() @pytest.mark.respx(base_url=base_url) - @pytest.mark.asyncio - async def test_client_response_validation_error(self, respx_mock: MockRouter) -> None: + async def test_client_response_validation_error( + self, respx_mock: MockRouter, async_client: AsyncBrowserbase + ) -> 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 self.client.get("/foo", cast_to=Model) + await async_client.get("/foo", cast_to=Model) assert isinstance(exc.value.__cause__, ValidationError) @@ -1492,7 +1530,6 @@ async def test_client_max_retries_validation(self) -> None: ) @pytest.mark.respx(base_url=base_url) - @pytest.mark.asyncio async def test_received_text_for_expected_json(self, respx_mock: MockRouter) -> None: class Model(BaseModel): name: str @@ -1504,11 +1541,14 @@ class Model(BaseModel): with pytest.raises(APIResponseValidationError): await strict_client.get("/foo", cast_to=Model) - client = AsyncBrowserbase(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 client.get("/foo", cast_to=Model) + 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", [ @@ -1531,13 +1571,12 @@ class Model(BaseModel): ], ) @mock.patch("time.time", mock.MagicMock(return_value=1696004797)) - @pytest.mark.asyncio - async def test_parse_retry_after_header(self, remaining_retries: int, retry_after: str, timeout: float) -> None: - client = AsyncBrowserbase(base_url=base_url, api_key=api_key, _strict_response_validation=True) - + async def test_parse_retry_after_header( + 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) - calculated = client._calculate_retry_timeout(remaining_retries, options, headers) + calculated = async_client._calculate_retry_timeout(remaining_retries, options, headers) assert calculated == pytest.approx(timeout, 0.5 * 0.875) # pyright: ignore[reportUnknownMemberType] @mock.patch("browserbase._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) @@ -1550,7 +1589,7 @@ async def test_retrying_timeout_errors_doesnt_leak( with pytest.raises(APITimeoutError): await async_client.sessions.with_streaming_response.create(project_id="projectId").__aenter__() - assert _get_open_connections(self.client) == 0 + assert _get_open_connections(async_client) == 0 @mock.patch("browserbase._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) @pytest.mark.respx(base_url=base_url) @@ -1561,12 +1600,11 @@ async def test_retrying_status_errors_doesnt_leak( with pytest.raises(APIStatusError): await async_client.sessions.with_streaming_response.create(project_id="projectId").__aenter__() - assert _get_open_connections(self.client) == 0 + assert _get_open_connections(async_client) == 0 @pytest.mark.parametrize("failures_before_success", [0, 2, 4]) @mock.patch("browserbase._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) @pytest.mark.respx(base_url=base_url) - @pytest.mark.asyncio @pytest.mark.parametrize("failure_mode", ["status", "exception"]) async def test_retries_taken( self, @@ -1598,7 +1636,6 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: @pytest.mark.parametrize("failures_before_success", [0, 2, 4]) @mock.patch("browserbase._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) @pytest.mark.respx(base_url=base_url) - @pytest.mark.asyncio async def test_omit_retry_count_header( self, async_client: AsyncBrowserbase, failures_before_success: int, respx_mock: MockRouter ) -> None: @@ -1624,7 +1661,6 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: @pytest.mark.parametrize("failures_before_success", [0, 2, 4]) @mock.patch("browserbase._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) @pytest.mark.respx(base_url=base_url) - @pytest.mark.asyncio async def test_overwrite_retry_count_header( self, async_client: AsyncBrowserbase, failures_before_success: int, respx_mock: MockRouter ) -> None: @@ -1674,26 +1710,26 @@ async def test_default_client_creation(self) -> None: ) @pytest.mark.respx(base_url=base_url) - async def test_follow_redirects(self, respx_mock: MockRouter) -> 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"}) ) respx_mock.get("/redirected").mock(return_value=httpx.Response(200, json={"status": "ok"})) - response = await self.client.post("/redirect", body={"key": "value"}, cast_to=httpx.Response) + 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) -> 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"}) ) with pytest.raises(APIStatusError) as exc_info: - await self.client.post( + await async_client.post( "/redirect", body={"key": "value"}, options={"follow_redirects": False}, cast_to=httpx.Response ) From 3cf0a3911608248bf5091102052d760d2732993a Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 4 Nov 2025 05:45:08 +0000 Subject: [PATCH 213/330] chore(internal): grammar fix (it's -> its) --- src/browserbase/_utils/_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/browserbase/_utils/_utils.py b/src/browserbase/_utils/_utils.py index 50d59269..eec7f4a1 100644 --- a/src/browserbase/_utils/_utils.py +++ b/src/browserbase/_utils/_utils.py @@ -133,7 +133,7 @@ def is_given(obj: _T | NotGiven | Omit) -> TypeGuard[_T]: # 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 it's place. +# 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 From 80866f2ac0b397933bd412f17c291405903508c9 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 11 Nov 2025 05:30:49 +0000 Subject: [PATCH 214/330] chore(package): drop Python 3.8 support --- README.md | 4 ++-- pyproject.toml | 5 ++--- src/browserbase/_utils/_sync.py | 34 +++------------------------------ 3 files changed, 7 insertions(+), 36 deletions(-) diff --git a/README.md b/README.md index 4edddc3e..69e519c2 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ [![PyPI version](https://img.shields.io/pypi/v/browserbase.svg?label=pypi%20(stable))](https://pypi.org/project/browserbase/) -The Browserbase Python library provides convenient access to the Browserbase REST API from any Python 3.8+ +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). @@ -405,7 +405,7 @@ print(browserbase.__version__) ## Requirements -Python 3.8 or higher. +Python 3.9 or higher. ## Contributing diff --git a/pyproject.toml b/pyproject.toml index e39b76a7..b8b27d81 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,11 +15,10 @@ dependencies = [ "distro>=1.7.0, <2", "sniffio", ] -requires-python = ">= 3.8" +requires-python = ">= 3.9" classifiers = [ "Typing :: Typed", "Intended Audience :: Developers", - "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", @@ -141,7 +140,7 @@ filterwarnings = [ # 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.8" +pythonVersion = "3.9" exclude = [ "_dev", diff --git a/src/browserbase/_utils/_sync.py b/src/browserbase/_utils/_sync.py index ad7ec71b..f6027c18 100644 --- a/src/browserbase/_utils/_sync.py +++ b/src/browserbase/_utils/_sync.py @@ -1,10 +1,8 @@ from __future__ import annotations -import sys import asyncio import functools -import contextvars -from typing import Any, TypeVar, Callable, Awaitable +from typing import TypeVar, Callable, Awaitable from typing_extensions import ParamSpec import anyio @@ -15,34 +13,11 @@ T_ParamSpec = ParamSpec("T_ParamSpec") -if sys.version_info >= (3, 9): - _asyncio_to_thread = asyncio.to_thread -else: - # backport of https://docs.python.org/3/library/asyncio-task.html#asyncio.to_thread - # for Python 3.8 support - async def _asyncio_to_thread( - func: Callable[T_ParamSpec, T_Retval], /, *args: T_ParamSpec.args, **kwargs: T_ParamSpec.kwargs - ) -> Any: - """Asynchronously run function *func* in a separate thread. - - Any *args and **kwargs supplied for this function are directly passed - to *func*. Also, the current :class:`contextvars.Context` is propagated, - allowing context variables from the main thread to be accessed in the - separate thread. - - Returns a coroutine that can be awaited to get the eventual result of *func*. - """ - loop = asyncio.events.get_running_loop() - ctx = contextvars.copy_context() - func_call = functools.partial(ctx.run, func, *args, **kwargs) - return await loop.run_in_executor(None, func_call) - - 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 asyncio.to_thread(func, *args, **kwargs) return await anyio.to_thread.run_sync( functools.partial(func, *args, **kwargs), @@ -53,10 +28,7 @@ async def to_thread( 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. For python version 3.9 and above, it uses - asyncio.to_thread to run the function in a separate thread. For python version - 3.8, it uses locally defined copy of the asyncio.to_thread function which was - introduced in python 3.9. + positional and keyword arguments. Usage: From 2ae61c399f139fad227547f1a56305deeacfd12e Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 11 Nov 2025 05:31:25 +0000 Subject: [PATCH 215/330] fix: compat with Python 3.14 --- src/browserbase/_models.py | 11 ++++++++--- tests/test_models.py | 8 ++++---- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/src/browserbase/_models.py b/src/browserbase/_models.py index 6a3cd1d2..fcec2cf9 100644 --- a/src/browserbase/_models.py +++ b/src/browserbase/_models.py @@ -2,6 +2,7 @@ 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 ( @@ -573,6 +574,9 @@ 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. @@ -615,8 +619,9 @@ def __init__( def _build_discriminated_union_meta(*, union: type, meta_annotations: tuple[Any, ...]) -> DiscriminatorDetails | None: - if isinstance(union, CachedDiscriminatorType): - return union.__discriminator__ + cached = DISCRIMINATOR_CACHE.get(union) + if cached is not None: + return cached discriminator_field_name: str | None = None @@ -669,7 +674,7 @@ def _build_discriminated_union_meta(*, union: type, meta_annotations: tuple[Any, discriminator_field=discriminator_field_name, discriminator_alias=discriminator_alias, ) - cast(CachedDiscriminatorType, union).__discriminator__ = details + DISCRIMINATOR_CACHE.setdefault(union, details) return details diff --git a/tests/test_models.py b/tests/test_models.py index 34f87334..1ecdeecf 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -9,7 +9,7 @@ from browserbase._utils import PropertyInfo from browserbase._compat import PYDANTIC_V1, parse_obj, model_dump, model_json -from browserbase._models import BaseModel, construct_type +from browserbase._models import DISCRIMINATOR_CACHE, BaseModel, construct_type class BasicModel(BaseModel): @@ -809,7 +809,7 @@ class B(BaseModel): UnionType = cast(Any, Union[A, B]) - assert not hasattr(UnionType, "__discriminator__") + assert not DISCRIMINATOR_CACHE.get(UnionType) m = construct_type( value={"type": "b", "data": "foo"}, type_=cast(Any, Annotated[UnionType, PropertyInfo(discriminator="type")]) @@ -818,7 +818,7 @@ class B(BaseModel): assert m.type == "b" assert m.data == "foo" # type: ignore[comparison-overlap] - discriminator = UnionType.__discriminator__ + discriminator = DISCRIMINATOR_CACHE.get(UnionType) assert discriminator is not None m = construct_type( @@ -830,7 +830,7 @@ class B(BaseModel): # if the discriminator details object stays the same between invocations then # we hit the cache - assert UnionType.__discriminator__ is discriminator + assert DISCRIMINATOR_CACHE.get(UnionType) is discriminator @pytest.mark.skipif(PYDANTIC_V1, reason="TypeAliasType is not supported in Pydantic v1") From 719303494808adc40ab3203bb1e1f4d0111ac8ac Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 12 Nov 2025 05:06:22 +0000 Subject: [PATCH 216/330] fix(compat): update signatures of `model_dump` and `model_dump_json` for Pydantic v1 --- src/browserbase/_models.py | 41 +++++++++++++++++++++++++++----------- 1 file changed, 29 insertions(+), 12 deletions(-) diff --git a/src/browserbase/_models.py b/src/browserbase/_models.py index fcec2cf9..ca9500b2 100644 --- a/src/browserbase/_models.py +++ b/src/browserbase/_models.py @@ -257,15 +257,16 @@ def model_dump( 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, - context: dict[str, Any] | None = None, - serialize_as_any: bool = False, 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 @@ -273,16 +274,24 @@ def model_dump( Args: mode: The mode in which `to_python` should run. - If mode is 'json', the dictionary will only contain JSON serializable types. - If mode is 'python', the dictionary may contain any Python objects. - include: A list of fields to include in the output. - exclude: A list of fields to exclude from the output. + 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 are unset or None from the output. - 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. - round_trip: Whether to enable serialization and deserialization round-trip support. - warnings: Whether to log warnings when invalid fields are encountered. + 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. @@ -299,6 +308,8 @@ def model_dump( 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, @@ -315,15 +326,17 @@ 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, - context: dict[str, Any] | None = None, fallback: Callable[[Any], Any] | None = None, serialize_as_any: bool = False, ) -> str: @@ -355,6 +368,10 @@ def model_dump_json( 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, From b8728a2192d4e069f8cf598a80856f83293ed808 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 22 Nov 2025 04:36:30 +0000 Subject: [PATCH 217/330] chore: add Python 3.14 classifier and testing --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index b8b27d81..e3b98444 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,6 +24,7 @@ classifiers = [ "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", From f846c084e57dbf11f718ef4fd381fb9b079ac918 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 28 Nov 2025 03:36:21 +0000 Subject: [PATCH 218/330] fix: ensure streams are always closed --- src/browserbase/_streaming.py | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/src/browserbase/_streaming.py b/src/browserbase/_streaming.py index 129714aa..d107619d 100644 --- a/src/browserbase/_streaming.py +++ b/src/browserbase/_streaming.py @@ -54,11 +54,12 @@ def __stream__(self) -> Iterator[_T]: process_data = self._client._process_response_data iterator = self._iter_events() - for sse in iterator: - yield process_data(data=sse.json(), cast_to=cast_to, response=response) - - # As we might not fully consume the response stream, we need to close it explicitly - response.close() + 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 @@ -117,11 +118,12 @@ async def __stream__(self) -> AsyncIterator[_T]: process_data = self._client._process_response_data iterator = self._iter_events() - async for sse in iterator: - yield process_data(data=sse.json(), cast_to=cast_to, response=response) - - # As we might not fully consume the response stream, we need to close it explicitly - await response.aclose() + 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 From d1d57a780d24eae1204c30ea127a1314e6aaa1b6 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 28 Nov 2025 03:37:32 +0000 Subject: [PATCH 219/330] chore(deps): mypy 1.18.1 has a regression, pin to 1.17 --- pyproject.toml | 2 +- requirements-dev.lock | 4 +++- requirements.lock | 8 ++++---- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index e3b98444..2964cfce 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -46,7 +46,7 @@ managed = true # version pins are in requirements-dev.lock dev-dependencies = [ "pyright==1.1.399", - "mypy", + "mypy==1.17", "respx", "pytest", "pytest-asyncio", diff --git a/requirements-dev.lock b/requirements-dev.lock index 64352852..e661b898 100644 --- a/requirements-dev.lock +++ b/requirements-dev.lock @@ -72,7 +72,7 @@ mdurl==0.1.2 multidict==6.4.4 # via aiohttp # via yarl -mypy==1.14.1 +mypy==1.17.0 mypy-extensions==1.0.0 # via mypy nodeenv==1.8.0 @@ -81,6 +81,8 @@ nox==2023.4.22 packaging==23.2 # via nox # via pytest +pathspec==0.12.1 + # via mypy platformdirs==3.11.0 # via virtualenv pluggy==1.5.0 diff --git a/requirements.lock b/requirements.lock index 55ea8833..335d50fe 100644 --- a/requirements.lock +++ b/requirements.lock @@ -55,21 +55,21 @@ multidict==6.4.4 propcache==0.3.1 # via aiohttp # via yarl -pydantic==2.11.9 +pydantic==2.12.5 # via browserbase -pydantic-core==2.33.2 +pydantic-core==2.41.5 # via pydantic sniffio==1.3.0 # via anyio # via browserbase -typing-extensions==4.12.2 +typing-extensions==4.15.0 # via anyio # via browserbase # via multidict # via pydantic # via pydantic-core # via typing-inspection -typing-inspection==0.4.1 +typing-inspection==0.4.2 # via pydantic yarl==1.20.0 # via aiohttp From ed19f730e66129284f3b3bf261214cd062e3d992 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 3 Dec 2025 08:29:14 +0000 Subject: [PATCH 220/330] chore: update lockfile --- pyproject.toml | 14 +++--- requirements-dev.lock | 108 +++++++++++++++++++++++------------------- requirements.lock | 31 ++++++------ 3 files changed, 83 insertions(+), 70 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 2964cfce..e28526c1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,14 +7,16 @@ license = "Apache-2.0" authors = [ { name = "Browserbase", email = "support@browserbase.com" }, ] + 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", + "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", diff --git a/requirements-dev.lock b/requirements-dev.lock index e661b898..95407eb0 100644 --- a/requirements-dev.lock +++ b/requirements-dev.lock @@ -12,40 +12,45 @@ -e file:. aiohappyeyeballs==2.6.1 # via aiohttp -aiohttp==3.12.8 +aiohttp==3.13.2 # via browserbase # via httpx-aiohttp -aiosignal==1.3.2 +aiosignal==1.4.0 # via aiohttp -annotated-types==0.6.0 +annotated-types==0.7.0 # via pydantic -anyio==4.4.0 +anyio==4.12.0 # via browserbase # via httpx -argcomplete==3.1.2 +argcomplete==3.6.3 # via nox async-timeout==5.0.1 # via aiohttp -attrs==25.3.0 +attrs==25.4.0 # via aiohttp -certifi==2023.7.22 + # via nox +backports-asyncio-runner==1.2.0 + # via pytest-asyncio +certifi==2025.11.12 # via httpcore # via httpx -colorlog==6.7.0 +colorlog==6.10.1 + # via nox +dependency-groups==1.3.1 # via nox -dirty-equals==0.6.0 -distlib==0.3.7 +dirty-equals==0.11 +distlib==0.4.0 # via virtualenv -distro==1.8.0 +distro==1.9.0 # via browserbase -exceptiongroup==1.2.2 +exceptiongroup==1.3.1 # via anyio # via pytest -execnet==2.1.1 +execnet==2.1.2 # via pytest-xdist -filelock==3.12.4 +filelock==3.19.1 # via virtualenv -frozenlist==1.6.2 +frozenlist==1.8.0 # via aiohttp # via aiosignal h11==0.16.0 @@ -58,82 +63,87 @@ httpx==0.28.1 # via respx httpx-aiohttp==0.1.9 # via browserbase -idna==3.4 +humanize==4.13.0 + # via nox +idna==3.11 # via anyio # via httpx # via yarl -importlib-metadata==7.0.0 -iniconfig==2.0.0 +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.4.4 +multidict==6.7.0 # via aiohttp # via yarl mypy==1.17.0 -mypy-extensions==1.0.0 +mypy-extensions==1.1.0 # via mypy -nodeenv==1.8.0 +nodeenv==1.9.1 # via pyright -nox==2023.4.22 -packaging==23.2 +nox==2025.11.12 +packaging==25.0 + # via dependency-groups # via nox # via pytest pathspec==0.12.1 # via mypy -platformdirs==3.11.0 +platformdirs==4.4.0 # via virtualenv -pluggy==1.5.0 +pluggy==1.6.0 # via pytest -propcache==0.3.1 +propcache==0.4.1 # via aiohttp # via yarl -pydantic==2.11.9 +pydantic==2.12.5 # via browserbase -pydantic-core==2.33.2 +pydantic-core==2.41.5 # via pydantic -pygments==2.18.0 +pygments==2.19.2 + # via pytest # via rich pyright==1.1.399 -pytest==8.3.3 +pytest==8.4.2 # via pytest-asyncio # via pytest-xdist -pytest-asyncio==0.24.0 -pytest-xdist==3.7.0 -python-dateutil==2.8.2 +pytest-asyncio==1.2.0 +pytest-xdist==3.8.0 +python-dateutil==2.9.0.post0 # via time-machine -pytz==2023.3.post1 - # via dirty-equals respx==0.22.0 -rich==13.7.1 -ruff==0.9.4 -setuptools==68.2.2 - # via nodeenv -six==1.16.0 +rich==14.2.0 +ruff==0.14.7 +six==1.17.0 # via python-dateutil -sniffio==1.3.0 - # via anyio +sniffio==1.3.1 # via browserbase -time-machine==2.9.0 -tomli==2.0.2 +time-machine==2.19.0 +tomli==2.3.0 + # via dependency-groups # via mypy + # via nox # via pytest -typing-extensions==4.12.2 +typing-extensions==4.15.0 + # via aiosignal # via anyio # via browserbase + # via exceptiongroup # via multidict # via mypy # via pydantic # via pydantic-core # via pyright + # via pytest-asyncio # via typing-inspection -typing-inspection==0.4.1 + # via virtualenv +typing-inspection==0.4.2 # via pydantic -virtualenv==20.24.5 +virtualenv==20.35.4 # via nox -yarl==1.20.0 +yarl==1.22.0 # via aiohttp -zipp==3.17.0 +zipp==3.23.0 # via importlib-metadata diff --git a/requirements.lock b/requirements.lock index 335d50fe..188c9eb1 100644 --- a/requirements.lock +++ b/requirements.lock @@ -12,28 +12,28 @@ -e file:. aiohappyeyeballs==2.6.1 # via aiohttp -aiohttp==3.12.8 +aiohttp==3.13.2 # via browserbase # via httpx-aiohttp -aiosignal==1.3.2 +aiosignal==1.4.0 # via aiohttp -annotated-types==0.6.0 +annotated-types==0.7.0 # via pydantic -anyio==4.4.0 +anyio==4.12.0 # via browserbase # via httpx async-timeout==5.0.1 # via aiohttp -attrs==25.3.0 +attrs==25.4.0 # via aiohttp -certifi==2023.7.22 +certifi==2025.11.12 # via httpcore # via httpx -distro==1.8.0 +distro==1.9.0 # via browserbase -exceptiongroup==1.2.2 +exceptiongroup==1.3.1 # via anyio -frozenlist==1.6.2 +frozenlist==1.8.0 # via aiohttp # via aiosignal h11==0.16.0 @@ -45,31 +45,32 @@ httpx==0.28.1 # via httpx-aiohttp httpx-aiohttp==0.1.9 # via browserbase -idna==3.4 +idna==3.11 # via anyio # via httpx # via yarl -multidict==6.4.4 +multidict==6.7.0 # via aiohttp # via yarl -propcache==0.3.1 +propcache==0.4.1 # via aiohttp # via yarl pydantic==2.12.5 # via browserbase pydantic-core==2.41.5 # via pydantic -sniffio==1.3.0 - # via anyio +sniffio==1.3.1 # via browserbase typing-extensions==4.15.0 + # via aiosignal # via anyio # via browserbase + # via exceptiongroup # via multidict # via pydantic # via pydantic-core # via typing-inspection typing-inspection==0.4.2 # via pydantic -yarl==1.20.0 +yarl==1.22.0 # via aiohttp From 34820acd3beac3d230f69ce1e88be1d3f3915c7e Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 3 Dec 2025 08:36:03 +0000 Subject: [PATCH 221/330] chore(docs): use environment variables for authentication in code snippets --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 69e519c2..f8431877 100644 --- a/README.md +++ b/README.md @@ -83,6 +83,7 @@ pip install --pre browserbase[aiohttp] Then you can enable it by instantiating the client with `http_client=DefaultAioHttpClient()`: ```python +import os import asyncio from browserbase import DefaultAioHttpClient from browserbase import AsyncBrowserbase @@ -90,7 +91,7 @@ from browserbase import AsyncBrowserbase async def main() -> None: async with AsyncBrowserbase( - api_key="My API Key", + api_key=os.environ.get("BROWSERBASE_API_KEY"), # This is the default and can be omitted http_client=DefaultAioHttpClient(), ) as client: session = await client.sessions.create( From 6c986c7e06696869c00ece9f57de6e450f728b58 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 9 Dec 2025 06:00:02 +0000 Subject: [PATCH 222/330] fix(types): allow pyright to infer TypedDict types within SequenceNotStr --- src/browserbase/_types.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/browserbase/_types.py b/src/browserbase/_types.py index f86be54d..0d54fc06 100644 --- a/src/browserbase/_types.py +++ b/src/browserbase/_types.py @@ -243,6 +243,9 @@ class HttpxSendArgs(TypedDict, total=False): 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: ... @@ -251,8 +254,6 @@ 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 index(self, value: Any, start: int = 0, stop: int = ..., /) -> int: ... - def count(self, value: Any, /) -> int: ... def __reversed__(self) -> Iterator[_T_co]: ... else: # just point this to a normal `Sequence` at runtime to avoid having to special case From dbf81921ac34f7f1efc785f7b397728f35adb914 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 9 Dec 2025 06:01:57 +0000 Subject: [PATCH 223/330] chore: add missing docstrings --- src/browserbase/types/session_create_params.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/browserbase/types/session_create_params.py b/src/browserbase/types/session_create_params.py index 3a517c06..2ba36400 100644 --- a/src/browserbase/types/session_create_params.py +++ b/src/browserbase/types/session_create_params.py @@ -85,6 +85,10 @@ class BrowserSettingsFingerprintScreen(TypedDict, total=False): class BrowserSettingsFingerprint(TypedDict, total=False): + """ + See usage examples [on the Stealth Mode page](/features/stealth-mode#fingerprinting) + """ + browsers: List[Literal["chrome", "edge", "firefox", "safari"]] devices: List[Literal["desktop", "mobile"]] @@ -160,6 +164,8 @@ class BrowserSettings(TypedDict, total=False): class ProxiesUnionMember0UnionMember0Geolocation(TypedDict, total=False): + """Geographic location for the proxy. Optional.""" + country: Required[str] """Country code in ISO 3166-1 alpha-2 format""" From 7ede1c639be8d492c1145637ec0e11ae5b287b7b Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 16 Dec 2025 05:35:51 +0000 Subject: [PATCH 224/330] chore(internal): add missing files argument to base client --- src/browserbase/_base_client.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/browserbase/_base_client.py b/src/browserbase/_base_client.py index 2485e4e6..1fdbc92b 100644 --- a/src/browserbase/_base_client.py +++ b/src/browserbase/_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 8b0df97ee123acf0773bd16e0694ed3479f5328d Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 17 Dec 2025 08:48:53 +0000 Subject: [PATCH 225/330] chore: speedup initial import --- src/browserbase/_client.py | 224 +++++++++++++++++++++++++++++-------- 1 file changed, 179 insertions(+), 45 deletions(-) diff --git a/src/browserbase/_client.py b/src/browserbase/_client.py index 8b54a5bb..5bb997a7 100644 --- a/src/browserbase/_client.py +++ b/src/browserbase/_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 contexts, projects, extensions from ._streaming import Stream as Stream, AsyncStream as AsyncStream from ._exceptions import APIStatusError, BrowserbaseError from ._base_client import ( @@ -29,7 +29,13 @@ SyncAPIClient, AsyncAPIClient, ) -from .resources.sessions import sessions + +if TYPE_CHECKING: + from .resources import contexts, projects, sessions, extensions + from .resources.contexts import ContextsResource, AsyncContextsResource + from .resources.projects import ProjectsResource, AsyncProjectsResource + from .resources.extensions import ExtensionsResource, AsyncExtensionsResource + from .resources.sessions.sessions import SessionsResource, AsyncSessionsResource __all__ = [ "Timeout", @@ -44,13 +50,6 @@ class Browserbase(SyncAPIClient): - contexts: contexts.ContextsResource - extensions: extensions.ExtensionsResource - projects: projects.ProjectsResource - sessions: sessions.SessionsResource - with_raw_response: BrowserbaseWithRawResponse - with_streaming_response: BrowserbaseWithStreamedResponse - # client options api_key: str @@ -105,12 +104,37 @@ def __init__( _strict_response_validation=_strict_response_validation, ) - self.contexts = contexts.ContextsResource(self) - self.extensions = extensions.ExtensionsResource(self) - self.projects = projects.ProjectsResource(self) - self.sessions = sessions.SessionsResource(self) - self.with_raw_response = BrowserbaseWithRawResponse(self) - self.with_streaming_response = BrowserbaseWithStreamedResponse(self) + @cached_property + def contexts(self) -> ContextsResource: + from .resources.contexts import ContextsResource + + return ContextsResource(self) + + @cached_property + def extensions(self) -> ExtensionsResource: + from .resources.extensions import ExtensionsResource + + return ExtensionsResource(self) + + @cached_property + def projects(self) -> ProjectsResource: + from .resources.projects import ProjectsResource + + return ProjectsResource(self) + + @cached_property + def sessions(self) -> SessionsResource: + from .resources.sessions import SessionsResource + + return SessionsResource(self) + + @cached_property + def with_raw_response(self) -> BrowserbaseWithRawResponse: + return BrowserbaseWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> BrowserbaseWithStreamedResponse: + return BrowserbaseWithStreamedResponse(self) @property @override @@ -218,13 +242,6 @@ def _make_status_error( class AsyncBrowserbase(AsyncAPIClient): - contexts: contexts.AsyncContextsResource - extensions: extensions.AsyncExtensionsResource - projects: projects.AsyncProjectsResource - sessions: sessions.AsyncSessionsResource - with_raw_response: AsyncBrowserbaseWithRawResponse - with_streaming_response: AsyncBrowserbaseWithStreamedResponse - # client options api_key: str @@ -279,12 +296,37 @@ def __init__( _strict_response_validation=_strict_response_validation, ) - self.contexts = contexts.AsyncContextsResource(self) - self.extensions = extensions.AsyncExtensionsResource(self) - self.projects = projects.AsyncProjectsResource(self) - self.sessions = sessions.AsyncSessionsResource(self) - self.with_raw_response = AsyncBrowserbaseWithRawResponse(self) - self.with_streaming_response = AsyncBrowserbaseWithStreamedResponse(self) + @cached_property + def contexts(self) -> AsyncContextsResource: + from .resources.contexts import AsyncContextsResource + + return AsyncContextsResource(self) + + @cached_property + def extensions(self) -> AsyncExtensionsResource: + from .resources.extensions import AsyncExtensionsResource + + return AsyncExtensionsResource(self) + + @cached_property + def projects(self) -> AsyncProjectsResource: + from .resources.projects import AsyncProjectsResource + + return AsyncProjectsResource(self) + + @cached_property + def sessions(self) -> AsyncSessionsResource: + from .resources.sessions import AsyncSessionsResource + + return AsyncSessionsResource(self) + + @cached_property + def with_raw_response(self) -> AsyncBrowserbaseWithRawResponse: + return AsyncBrowserbaseWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncBrowserbaseWithStreamedResponse: + return AsyncBrowserbaseWithStreamedResponse(self) @property @override @@ -392,35 +434,127 @@ def _make_status_error( class BrowserbaseWithRawResponse: + _client: Browserbase + def __init__(self, client: Browserbase) -> None: - self.contexts = contexts.ContextsResourceWithRawResponse(client.contexts) - self.extensions = extensions.ExtensionsResourceWithRawResponse(client.extensions) - self.projects = projects.ProjectsResourceWithRawResponse(client.projects) - self.sessions = sessions.SessionsResourceWithRawResponse(client.sessions) + self._client = client + + @cached_property + def contexts(self) -> contexts.ContextsResourceWithRawResponse: + from .resources.contexts import ContextsResourceWithRawResponse + + return ContextsResourceWithRawResponse(self._client.contexts) + + @cached_property + def extensions(self) -> extensions.ExtensionsResourceWithRawResponse: + from .resources.extensions import ExtensionsResourceWithRawResponse + + return ExtensionsResourceWithRawResponse(self._client.extensions) + + @cached_property + def projects(self) -> projects.ProjectsResourceWithRawResponse: + from .resources.projects import ProjectsResourceWithRawResponse + + return ProjectsResourceWithRawResponse(self._client.projects) + + @cached_property + def sessions(self) -> sessions.SessionsResourceWithRawResponse: + from .resources.sessions import SessionsResourceWithRawResponse + + return SessionsResourceWithRawResponse(self._client.sessions) class AsyncBrowserbaseWithRawResponse: + _client: AsyncBrowserbase + def __init__(self, client: AsyncBrowserbase) -> None: - self.contexts = contexts.AsyncContextsResourceWithRawResponse(client.contexts) - self.extensions = extensions.AsyncExtensionsResourceWithRawResponse(client.extensions) - self.projects = projects.AsyncProjectsResourceWithRawResponse(client.projects) - self.sessions = sessions.AsyncSessionsResourceWithRawResponse(client.sessions) + self._client = client + + @cached_property + def contexts(self) -> contexts.AsyncContextsResourceWithRawResponse: + from .resources.contexts import AsyncContextsResourceWithRawResponse + + return AsyncContextsResourceWithRawResponse(self._client.contexts) + + @cached_property + def extensions(self) -> extensions.AsyncExtensionsResourceWithRawResponse: + from .resources.extensions import AsyncExtensionsResourceWithRawResponse + + return AsyncExtensionsResourceWithRawResponse(self._client.extensions) + + @cached_property + def projects(self) -> projects.AsyncProjectsResourceWithRawResponse: + from .resources.projects import AsyncProjectsResourceWithRawResponse + + return AsyncProjectsResourceWithRawResponse(self._client.projects) + + @cached_property + def sessions(self) -> sessions.AsyncSessionsResourceWithRawResponse: + from .resources.sessions import AsyncSessionsResourceWithRawResponse + + return AsyncSessionsResourceWithRawResponse(self._client.sessions) class BrowserbaseWithStreamedResponse: + _client: Browserbase + def __init__(self, client: Browserbase) -> None: - self.contexts = contexts.ContextsResourceWithStreamingResponse(client.contexts) - self.extensions = extensions.ExtensionsResourceWithStreamingResponse(client.extensions) - self.projects = projects.ProjectsResourceWithStreamingResponse(client.projects) - self.sessions = sessions.SessionsResourceWithStreamingResponse(client.sessions) + self._client = client + + @cached_property + def contexts(self) -> contexts.ContextsResourceWithStreamingResponse: + from .resources.contexts import ContextsResourceWithStreamingResponse + + return ContextsResourceWithStreamingResponse(self._client.contexts) + + @cached_property + def extensions(self) -> extensions.ExtensionsResourceWithStreamingResponse: + from .resources.extensions import ExtensionsResourceWithStreamingResponse + + return ExtensionsResourceWithStreamingResponse(self._client.extensions) + + @cached_property + def projects(self) -> projects.ProjectsResourceWithStreamingResponse: + from .resources.projects import ProjectsResourceWithStreamingResponse + + return ProjectsResourceWithStreamingResponse(self._client.projects) + + @cached_property + def sessions(self) -> sessions.SessionsResourceWithStreamingResponse: + from .resources.sessions import SessionsResourceWithStreamingResponse + + return SessionsResourceWithStreamingResponse(self._client.sessions) class AsyncBrowserbaseWithStreamedResponse: + _client: AsyncBrowserbase + def __init__(self, client: AsyncBrowserbase) -> None: - self.contexts = contexts.AsyncContextsResourceWithStreamingResponse(client.contexts) - self.extensions = extensions.AsyncExtensionsResourceWithStreamingResponse(client.extensions) - self.projects = projects.AsyncProjectsResourceWithStreamingResponse(client.projects) - self.sessions = sessions.AsyncSessionsResourceWithStreamingResponse(client.sessions) + self._client = client + + @cached_property + def contexts(self) -> contexts.AsyncContextsResourceWithStreamingResponse: + from .resources.contexts import AsyncContextsResourceWithStreamingResponse + + return AsyncContextsResourceWithStreamingResponse(self._client.contexts) + + @cached_property + def extensions(self) -> extensions.AsyncExtensionsResourceWithStreamingResponse: + from .resources.extensions import AsyncExtensionsResourceWithStreamingResponse + + return AsyncExtensionsResourceWithStreamingResponse(self._client.extensions) + + @cached_property + def projects(self) -> projects.AsyncProjectsResourceWithStreamingResponse: + from .resources.projects import AsyncProjectsResourceWithStreamingResponse + + return AsyncProjectsResourceWithStreamingResponse(self._client.projects) + + @cached_property + def sessions(self) -> sessions.AsyncSessionsResourceWithStreamingResponse: + from .resources.sessions import AsyncSessionsResourceWithStreamingResponse + + return AsyncSessionsResourceWithStreamingResponse(self._client.sessions) Client = Browserbase From 5cad7c563e665f3b4ea99baafa566831309ec587 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 18 Dec 2025 10:33:56 +0000 Subject: [PATCH 226/330] fix: use async_to_httpx_files in patch method --- src/browserbase/_base_client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/browserbase/_base_client.py b/src/browserbase/_base_client.py index 1fdbc92b..eea38088 100644 --- a/src/browserbase/_base_client.py +++ b/src/browserbase/_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 4b6519381c2b25fa9879bca72686bed1ec797d99 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 19 Dec 2025 09:03:23 +0000 Subject: [PATCH 227/330] 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 feccbdde..7bc921af 100755 --- a/scripts/lint +++ b/scripts/lint @@ -4,8 +4,13 @@ set -e cd "$(dirname "$0")/.." -echo "==> Running lints" -rye run lint +if [ "$1" = "--fix" ]; then + echo "==> Running lints with --fix" + rye run fix:ruff +else + echo "==> Running lints" + rye run lint +fi echo "==> Making sure it imports" rye run python -c 'import browserbase' From 40f9c216fd9053e7c813bc7f0db47ca98dc6f6da Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 5 Jan 2026 11:25:10 +0000 Subject: [PATCH 228/330] feat(api): api update --- LICENSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE b/LICENSE index 2cec9d4b..9d3232f7 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 2026 Browserbase Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. From f5577f64d7b93de8fc5375c165731481035e37fb Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 14 Jan 2026 13:19:07 +0000 Subject: [PATCH 229/330] chore(internal): codegen related update --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index f8431877..dcb4b768 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ The REST API documentation can be found on [docs.browserbase.com](https://docs.b ```sh # install from PyPI -pip install --pre browserbase +pip install '--pre browserbase' ``` ## Usage @@ -77,7 +77,7 @@ You can enable this by installing `aiohttp`: ```sh # install from PyPI -pip install --pre browserbase[aiohttp] +pip install '--pre browserbase[aiohttp]' ``` Then you can enable it by instantiating the client with `http_client=DefaultAioHttpClient()`: From 5882740f29f6c4bb3978a44a216db45b409d3f0e Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 14 Jan 2026 13:25:23 +0000 Subject: [PATCH 230/330] feat(client): add support for binary request streaming --- src/browserbase/_base_client.py | 145 +++++++++++++++++++++++-- src/browserbase/_models.py | 17 ++- src/browserbase/_types.py | 9 ++ tests/test_client.py | 187 +++++++++++++++++++++++++++++++- 4 files changed, 344 insertions(+), 14 deletions(-) diff --git a/src/browserbase/_base_client.py b/src/browserbase/_base_client.py index eea38088..6621d6ca 100644 --- a/src/browserbase/_base_client.py +++ b/src/browserbase/_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/browserbase/_models.py b/src/browserbase/_models.py index ca9500b2..29070e05 100644 --- a/src/browserbase/_models.py +++ b/src/browserbase/_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/browserbase/_types.py b/src/browserbase/_types.py index 0d54fc06..abefae08 100644 --- a/src/browserbase/_types.py +++ b/src/browserbase/_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 db556fc2..608cc1f9 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 @@ -36,6 +37,7 @@ from .utils import update_env +T = TypeVar("T") base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") api_key = "My API Key" @@ -50,6 +52,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: Browserbase | AsyncBrowserbase) -> int: transport = client._client._transport assert isinstance(transport, httpx.HTTPTransport) or isinstance(transport, httpx.AsyncHTTPTransport) @@ -502,6 +555,70 @@ def test_multipart_repeating_array(self, client: Browserbase) -> None: b"", ] + @pytest.mark.respx(base_url=base_url) + def test_binary_content_upload(self, respx_mock: MockRouter, client: Browserbase) -> 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 Browserbase( + base_url=base_url, + api_key=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: Browserbase) -> 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: Browserbase) -> None: class Model1(BaseModel): @@ -1335,6 +1452,72 @@ def test_multipart_repeating_array(self, async_client: AsyncBrowserbase) -> None b"", ] + @pytest.mark.respx(base_url=base_url) + async def test_binary_content_upload(self, respx_mock: MockRouter, async_client: AsyncBrowserbase) -> 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 AsyncBrowserbase( + base_url=base_url, + api_key=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: AsyncBrowserbase + ) -> 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: AsyncBrowserbase) -> None: class Model1(BaseModel): From f6237d922766fac6481aac5f03ae7fedaad45bc8 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 17 Jan 2026 09:34:51 +0000 Subject: [PATCH 231/330] chore(internal): update `actions/checkout` version --- .github/workflows/ci.yml | 6 +++--- .github/workflows/publish-pypi.yml | 2 +- .github/workflows/release-doctor.yml | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8edf5a60..720a983c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,7 +19,7 @@ jobs: runs-on: ${{ github.repository == 'stainless-sdks/browserbase-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} if: github.event_name == 'push' || github.event.pull_request.head.repo.fork steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Install Rye run: | @@ -44,7 +44,7 @@ jobs: id-token: write runs-on: ${{ github.repository == 'stainless-sdks/browserbase-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Install Rye run: | @@ -81,7 +81,7 @@ jobs: runs-on: ${{ github.repository == 'stainless-sdks/browserbase-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} if: github.event_name == 'push' || github.event.pull_request.head.repo.fork steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Install Rye run: | diff --git a/.github/workflows/publish-pypi.yml b/.github/workflows/publish-pypi.yml index b3c832c7..7fb6d449 100644 --- a/.github/workflows/publish-pypi.yml +++ b/.github/workflows/publish-pypi.yml @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Install Rye run: | diff --git a/.github/workflows/release-doctor.yml b/.github/workflows/release-doctor.yml index 3e17e458..5beedb0d 100644 --- a/.github/workflows/release-doctor.yml +++ b/.github/workflows/release-doctor.yml @@ -12,7 +12,7 @@ jobs: if: github.repository == 'browserbase/sdk-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 + - uses: actions/checkout@v6 - name: Check release environment run: | From 3aa179859b2677e65d4a5dd23696991139b93148 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 24 Jan 2026 07:42:28 +0000 Subject: [PATCH 232/330] chore(ci): upgrade `actions/github-script` --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 720a983c..c77d6d73 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -63,7 +63,7 @@ jobs: - name: Get GitHub OIDC Token if: github.repository == 'stainless-sdks/browserbase-python' id: github-oidc - uses: actions/github-script@v6 + uses: actions/github-script@v8 with: script: core.setOutput('github_token', await core.getIDToken()); From 5bbb9e33c40319ef8fa628b1ca7803a9749cb354 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 30 Jan 2026 07:15:31 +0000 Subject: [PATCH 233/330] feat(client): add custom JSON encoder for extended type support --- src/browserbase/_base_client.py | 7 +- src/browserbase/_compat.py | 6 +- src/browserbase/_utils/_json.py | 35 +++++++++ tests/test_utils/test_json.py | 126 ++++++++++++++++++++++++++++++++ 4 files changed, 169 insertions(+), 5 deletions(-) create mode 100644 src/browserbase/_utils/_json.py create mode 100644 tests/test_utils/test_json.py diff --git a/src/browserbase/_base_client.py b/src/browserbase/_base_client.py index 6621d6ca..5bc9823d 100644 --- a/src/browserbase/_base_client.py +++ b/src/browserbase/_base_client.py @@ -86,6 +86,7 @@ APIConnectionError, APIResponseValidationError, ) +from ._utils._json import openapi_dumps log: logging.Logger = logging.getLogger(__name__) @@ -554,8 +555,10 @@ def _build_request( 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 + elif not files: + # Don't set content when JSON is sent as multipart/form-data, + # since httpx's content param overrides other body arguments + kwargs["content"] = openapi_dumps(json_data) if is_given(json_data) and json_data is not None else None kwargs["files"] = files else: headers.pop("Content-Type", None) diff --git a/src/browserbase/_compat.py b/src/browserbase/_compat.py index bdef67f0..786ff42a 100644 --- a/src/browserbase/_compat.py +++ b/src/browserbase/_compat.py @@ -139,6 +139,7 @@ def model_dump( exclude_defaults: bool = False, warnings: bool = True, mode: Literal["json", "python"] = "python", + by_alias: bool | None = None, ) -> dict[str, Any]: if (not PYDANTIC_V1) or hasattr(model, "model_dump"): return model.model_dump( @@ -148,13 +149,12 @@ def model_dump( exclude_defaults=exclude_defaults, # warnings are not supported in Pydantic v1 warnings=True if PYDANTIC_V1 else warnings, + by_alias=by_alias, ) return cast( "dict[str, Any]", model.dict( # pyright: ignore[reportDeprecated, reportUnnecessaryCast] - exclude=exclude, - exclude_unset=exclude_unset, - exclude_defaults=exclude_defaults, + exclude=exclude, exclude_unset=exclude_unset, exclude_defaults=exclude_defaults, by_alias=bool(by_alias) ), ) diff --git a/src/browserbase/_utils/_json.py b/src/browserbase/_utils/_json.py new file mode 100644 index 00000000..60584214 --- /dev/null +++ b/src/browserbase/_utils/_json.py @@ -0,0 +1,35 @@ +import json +from typing import Any +from datetime import datetime +from typing_extensions import override + +import pydantic + +from .._compat import model_dump + + +def openapi_dumps(obj: Any) -> bytes: + """ + Serialize an object to UTF-8 encoded JSON bytes. + + Extends the standard json.dumps with support for additional types + commonly used in the SDK, such as `datetime`, `pydantic.BaseModel`, etc. + """ + return json.dumps( + obj, + cls=_CustomEncoder, + # Uses the same defaults as httpx's JSON serialization + ensure_ascii=False, + separators=(",", ":"), + allow_nan=False, + ).encode() + + +class _CustomEncoder(json.JSONEncoder): + @override + def default(self, o: Any) -> Any: + if isinstance(o, datetime): + return o.isoformat() + if isinstance(o, pydantic.BaseModel): + return model_dump(o, exclude_unset=True, mode="json", by_alias=True) + return super().default(o) diff --git a/tests/test_utils/test_json.py b/tests/test_utils/test_json.py new file mode 100644 index 00000000..9cf7b782 --- /dev/null +++ b/tests/test_utils/test_json.py @@ -0,0 +1,126 @@ +from __future__ import annotations + +import datetime +from typing import Union + +import pydantic + +from browserbase import _compat +from browserbase._utils._json import openapi_dumps + + +class TestOpenapiDumps: + def test_basic(self) -> None: + data = {"key": "value", "number": 42} + json_bytes = openapi_dumps(data) + assert json_bytes == b'{"key":"value","number":42}' + + def test_datetime_serialization(self) -> None: + dt = datetime.datetime(2023, 1, 1, 12, 0, 0) + data = {"datetime": dt} + json_bytes = openapi_dumps(data) + assert json_bytes == b'{"datetime":"2023-01-01T12:00:00"}' + + def test_pydantic_model_serialization(self) -> None: + class User(pydantic.BaseModel): + first_name: str + last_name: str + age: int + + model_instance = User(first_name="John", last_name="Kramer", age=83) + data = {"model": model_instance} + json_bytes = openapi_dumps(data) + assert json_bytes == b'{"model":{"first_name":"John","last_name":"Kramer","age":83}}' + + def test_pydantic_model_with_default_values(self) -> None: + class User(pydantic.BaseModel): + name: str + role: str = "user" + active: bool = True + score: int = 0 + + model_instance = User(name="Alice") + data = {"model": model_instance} + json_bytes = openapi_dumps(data) + assert json_bytes == b'{"model":{"name":"Alice"}}' + + def test_pydantic_model_with_default_values_overridden(self) -> None: + class User(pydantic.BaseModel): + name: str + role: str = "user" + active: bool = True + + model_instance = User(name="Bob", role="admin", active=False) + data = {"model": model_instance} + json_bytes = openapi_dumps(data) + assert json_bytes == b'{"model":{"name":"Bob","role":"admin","active":false}}' + + def test_pydantic_model_with_alias(self) -> None: + class User(pydantic.BaseModel): + first_name: str = pydantic.Field(alias="firstName") + last_name: str = pydantic.Field(alias="lastName") + + model_instance = User(firstName="John", lastName="Doe") + data = {"model": model_instance} + json_bytes = openapi_dumps(data) + assert json_bytes == b'{"model":{"firstName":"John","lastName":"Doe"}}' + + def test_pydantic_model_with_alias_and_default(self) -> None: + class User(pydantic.BaseModel): + user_name: str = pydantic.Field(alias="userName") + user_role: str = pydantic.Field(default="member", alias="userRole") + is_active: bool = pydantic.Field(default=True, alias="isActive") + + model_instance = User(userName="charlie") + data = {"model": model_instance} + json_bytes = openapi_dumps(data) + assert json_bytes == b'{"model":{"userName":"charlie"}}' + + model_with_overrides = User(userName="diana", userRole="admin", isActive=False) + data = {"model": model_with_overrides} + json_bytes = openapi_dumps(data) + assert json_bytes == b'{"model":{"userName":"diana","userRole":"admin","isActive":false}}' + + def test_pydantic_model_with_nested_models_and_defaults(self) -> None: + class Address(pydantic.BaseModel): + street: str + city: str = "Unknown" + + class User(pydantic.BaseModel): + name: str + address: Address + verified: bool = False + + if _compat.PYDANTIC_V1: + # to handle forward references in Pydantic v1 + User.update_forward_refs(**locals()) # type: ignore[reportDeprecated] + + address = Address(street="123 Main St") + user = User(name="Diana", address=address) + data = {"user": user} + json_bytes = openapi_dumps(data) + assert json_bytes == b'{"user":{"name":"Diana","address":{"street":"123 Main St"}}}' + + address_with_city = Address(street="456 Oak Ave", city="Boston") + user_verified = User(name="Eve", address=address_with_city, verified=True) + data = {"user": user_verified} + json_bytes = openapi_dumps(data) + assert ( + json_bytes == b'{"user":{"name":"Eve","address":{"street":"456 Oak Ave","city":"Boston"},"verified":true}}' + ) + + def test_pydantic_model_with_optional_fields(self) -> None: + class User(pydantic.BaseModel): + name: str + email: Union[str, None] + phone: Union[str, None] + + model_with_none = User(name="Eve", email=None, phone=None) + data = {"model": model_with_none} + json_bytes = openapi_dumps(data) + assert json_bytes == b'{"model":{"name":"Eve","email":null,"phone":null}}' + + model_with_values = User(name="Frank", email="frank@example.com", phone=None) + data = {"model": model_with_values} + json_bytes = openapi_dumps(data) + assert json_bytes == b'{"model":{"name":"Frank","email":"frank@example.com","phone":null}}' From 8d5b36df00a03ebaf30a82b25adcbd024369028b Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 10 Feb 2026 07:56:05 +0000 Subject: [PATCH 234/330] chore(internal): bump dependencies --- requirements-dev.lock | 20 ++++++++++---------- requirements.lock | 8 ++++---- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/requirements-dev.lock b/requirements-dev.lock index 95407eb0..e7b4891b 100644 --- a/requirements-dev.lock +++ b/requirements-dev.lock @@ -12,14 +12,14 @@ -e file:. aiohappyeyeballs==2.6.1 # via aiohttp -aiohttp==3.13.2 +aiohttp==3.13.3 # via browserbase # via httpx-aiohttp aiosignal==1.4.0 # via aiohttp annotated-types==0.7.0 # via pydantic -anyio==4.12.0 +anyio==4.12.1 # via browserbase # via httpx argcomplete==3.6.3 @@ -31,7 +31,7 @@ attrs==25.4.0 # via nox backports-asyncio-runner==1.2.0 # via pytest-asyncio -certifi==2025.11.12 +certifi==2026.1.4 # via httpcore # via httpx colorlog==6.10.1 @@ -61,7 +61,7 @@ httpx==0.28.1 # via browserbase # via httpx-aiohttp # via respx -httpx-aiohttp==0.1.9 +httpx-aiohttp==0.1.12 # via browserbase humanize==4.13.0 # via nox @@ -69,7 +69,7 @@ idna==3.11 # via anyio # via httpx # via yarl -importlib-metadata==8.7.0 +importlib-metadata==8.7.1 iniconfig==2.1.0 # via pytest markdown-it-py==3.0.0 @@ -82,14 +82,14 @@ multidict==6.7.0 mypy==1.17.0 mypy-extensions==1.1.0 # via mypy -nodeenv==1.9.1 +nodeenv==1.10.0 # via pyright nox==2025.11.12 packaging==25.0 # via dependency-groups # via nox # via pytest -pathspec==0.12.1 +pathspec==1.0.3 # via mypy platformdirs==4.4.0 # via virtualenv @@ -115,13 +115,13 @@ python-dateutil==2.9.0.post0 # via time-machine respx==0.22.0 rich==14.2.0 -ruff==0.14.7 +ruff==0.14.13 six==1.17.0 # via python-dateutil sniffio==1.3.1 # via browserbase time-machine==2.19.0 -tomli==2.3.0 +tomli==2.4.0 # via dependency-groups # via mypy # via nox @@ -141,7 +141,7 @@ typing-extensions==4.15.0 # via virtualenv typing-inspection==0.4.2 # via pydantic -virtualenv==20.35.4 +virtualenv==20.36.1 # via nox yarl==1.22.0 # via aiohttp diff --git a/requirements.lock b/requirements.lock index 188c9eb1..ba67caa6 100644 --- a/requirements.lock +++ b/requirements.lock @@ -12,21 +12,21 @@ -e file:. aiohappyeyeballs==2.6.1 # via aiohttp -aiohttp==3.13.2 +aiohttp==3.13.3 # via browserbase # via httpx-aiohttp aiosignal==1.4.0 # via aiohttp annotated-types==0.7.0 # via pydantic -anyio==4.12.0 +anyio==4.12.1 # via browserbase # via httpx async-timeout==5.0.1 # via aiohttp attrs==25.4.0 # via aiohttp -certifi==2025.11.12 +certifi==2026.1.4 # via httpcore # via httpx distro==1.9.0 @@ -43,7 +43,7 @@ httpcore==1.0.9 httpx==0.28.1 # via browserbase # via httpx-aiohttp -httpx-aiohttp==0.1.9 +httpx-aiohttp==0.1.12 # via browserbase idna==3.11 # via anyio From d94e09d44df92b9ac7c3c419bc64bb74f6968ef7 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 12 Feb 2026 10:36:57 +0000 Subject: [PATCH 235/330] chore(internal): fix lint error on Python 3.14 --- src/browserbase/_utils/_compat.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/browserbase/_utils/_compat.py b/src/browserbase/_utils/_compat.py index dd703233..2c70b299 100644 --- a/src/browserbase/_utils/_compat.py +++ b/src/browserbase/_utils/_compat.py @@ -26,7 +26,7 @@ def is_union(tp: Optional[Type[Any]]) -> bool: else: import types - return tp is Union or tp is types.UnionType + return tp is Union or tp is types.UnionType # type: ignore[comparison-overlap] def is_typeddict(tp: Type[Any]) -> bool: From 4265c05d8b385a9f535c36aa5ca0e2967bc1981c Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 13 Feb 2026 06:23:40 +0000 Subject: [PATCH 236/330] chore: format all `api.md` files --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index e28526c1..340f3b38 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -69,7 +69,7 @@ format = { chain = [ # 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:docs" = "bash -c 'python scripts/utils/ruffen-docs.py README.md $(find . -type f -name api.md)'" "format:ruff" = "ruff format" "lint" = { chain = [ From 3f8211dd67bab8de25404158749160bb03c26f70 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 20 Feb 2026 09:38:24 +0000 Subject: [PATCH 237/330] chore: update mock server docs --- CONTRIBUTING.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5f8bfea6..4f99ed67 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -88,8 +88,7 @@ $ pip install ./path-to-wheel-file.whl 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 +$ ./scripts/mock ``` ```sh From 9db7aa77e6d4e43c63d2a5296cd9116334f4c889 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 23 Feb 2026 22:27:36 +0000 Subject: [PATCH 238/330] feat(api): manual updates --- .stats.yml | 4 +- README.md | 1 - src/browserbase/resources/contexts.py | 6 +- .../resources/sessions/sessions.py | 58 +++++++++---------- .../types/context_create_params.py | 4 +- .../types/session_create_params.py | 28 ++++++--- .../types/session_update_params.py | 12 ++-- tests/api_resources/test_contexts.py | 26 +++++---- tests/api_resources/test_sessions.py | 54 +++++++++-------- tests/test_client.py | 28 ++++----- 10 files changed, 112 insertions(+), 109 deletions(-) diff --git a/.stats.yml b/.stats.yml index b000c8c1..766f657d 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 18 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fbrowserbase-be7a4aeebb1605262935b4b3ab446a95b1fad8a7d18098943dd548c8a486ef13.yml -openapi_spec_hash: 1c950a109f80140711e7ae2cf87fddad +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fbrowserbase-43f4549d7362ccad24d8f6124e73023a6cd5b62dab9c48cf3173435ca2f23155.yml +openapi_spec_hash: 920add25d34d7a858dad464ea558ce48 config_hash: b3ca4ec5b02e5333af51ebc2e9fdef1b diff --git a/README.md b/README.md index dcb4b768..5d384354 100644 --- a/README.md +++ b/README.md @@ -122,7 +122,6 @@ from browserbase import Browserbase client = Browserbase() session = client.sessions.create( - project_id="projectId", browser_settings={}, ) print(session.browser_settings) diff --git a/src/browserbase/resources/contexts.py b/src/browserbase/resources/contexts.py index d2bb4167..d7561cf3 100644 --- a/src/browserbase/resources/contexts.py +++ b/src/browserbase/resources/contexts.py @@ -5,7 +5,7 @@ import httpx from ..types import context_create_params -from .._types import Body, Query, Headers, NotGiven, not_given +from .._types import Body, Omit, Query, Headers, NotGiven, omit, not_given from .._utils import maybe_transform, async_maybe_transform from .._compat import cached_property from .._resource import SyncAPIResource, AsyncAPIResource @@ -46,7 +46,7 @@ def with_streaming_response(self) -> ContextsResourceWithStreamingResponse: def create( self, *, - project_id: str, + project_id: str | 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, @@ -169,7 +169,7 @@ def with_streaming_response(self) -> AsyncContextsResourceWithStreamingResponse: async def create( self, *, - project_id: str, + project_id: str | 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, diff --git a/src/browserbase/resources/sessions/sessions.py b/src/browserbase/resources/sessions/sessions.py index ceaaeb81..60554305 100644 --- a/src/browserbase/resources/sessions/sessions.py +++ b/src/browserbase/resources/sessions/sessions.py @@ -99,10 +99,10 @@ def with_streaming_response(self) -> SessionsResourceWithStreamingResponse: def create( self, *, - project_id: str, browser_settings: session_create_params.BrowserSettings | Omit = omit, extension_id: str | Omit = omit, keep_alive: bool | Omit = omit, + project_id: str | Omit = omit, proxies: Union[Iterable[session_create_params.ProxiesUnionMember0], bool] | Omit = omit, region: Literal["us-west-2", "us-east-1", "eu-central-1", "ap-southeast-1"] | Omit = omit, api_timeout: int | Omit = omit, @@ -117,17 +117,17 @@ def create( """Create a Session Args: - project_id: The Project ID. + extension_id: The uploaded Extension ID. - Can be found in - [Settings](https://www.browserbase.com/settings). - - extension_id: The uploaded Extension ID. See + See [Upload Extension](/reference/api/upload-an-extension). keep_alive: Set to true to keep the session alive even after disconnections. Available on the Hobby Plan and above. + project_id: The Project ID. Can be found in + [Settings](https://www.browserbase.com/settings). + proxies: Proxy configuration. Can be true for default proxy, or an array of proxy configurations. @@ -151,10 +151,10 @@ def create( "/v1/sessions", body=maybe_transform( { - "project_id": project_id, "browser_settings": browser_settings, "extension_id": extension_id, "keep_alive": keep_alive, + "project_id": project_id, "proxies": proxies, "region": region, "api_timeout": api_timeout, @@ -205,8 +205,8 @@ def update( self, id: str, *, - project_id: str, status: Literal["REQUEST_RELEASE"], + project_id: str | 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, @@ -214,17 +214,16 @@ def update( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> SessionUpdateResponse: - """Update a Session + """ + Update a Session Args: - project_id: The Project ID. - - Can be found in - [Settings](https://www.browserbase.com/settings). - status: Set to `REQUEST_RELEASE` to request that the session complete. Use before session's timeout to avoid additional charges. + project_id: The Project ID. Can be found in + [Settings](https://www.browserbase.com/settings). + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -239,8 +238,8 @@ def update( f"/v1/sessions/{id}", body=maybe_transform( { - "project_id": project_id, "status": status, + "project_id": project_id, }, session_update_params.SessionUpdateParams, ), @@ -370,10 +369,10 @@ def with_streaming_response(self) -> AsyncSessionsResourceWithStreamingResponse: async def create( self, *, - project_id: str, browser_settings: session_create_params.BrowserSettings | Omit = omit, extension_id: str | Omit = omit, keep_alive: bool | Omit = omit, + project_id: str | Omit = omit, proxies: Union[Iterable[session_create_params.ProxiesUnionMember0], bool] | Omit = omit, region: Literal["us-west-2", "us-east-1", "eu-central-1", "ap-southeast-1"] | Omit = omit, api_timeout: int | Omit = omit, @@ -388,17 +387,17 @@ async def create( """Create a Session Args: - project_id: The Project ID. + extension_id: The uploaded Extension ID. - Can be found in - [Settings](https://www.browserbase.com/settings). - - extension_id: The uploaded Extension ID. See + See [Upload Extension](/reference/api/upload-an-extension). keep_alive: Set to true to keep the session alive even after disconnections. Available on the Hobby Plan and above. + project_id: The Project ID. Can be found in + [Settings](https://www.browserbase.com/settings). + proxies: Proxy configuration. Can be true for default proxy, or an array of proxy configurations. @@ -422,10 +421,10 @@ async def create( "/v1/sessions", body=await async_maybe_transform( { - "project_id": project_id, "browser_settings": browser_settings, "extension_id": extension_id, "keep_alive": keep_alive, + "project_id": project_id, "proxies": proxies, "region": region, "api_timeout": api_timeout, @@ -476,8 +475,8 @@ async def update( self, id: str, *, - project_id: str, status: Literal["REQUEST_RELEASE"], + project_id: str | 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, @@ -485,17 +484,16 @@ async def update( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> SessionUpdateResponse: - """Update a Session + """ + Update a Session Args: - project_id: The Project ID. - - Can be found in - [Settings](https://www.browserbase.com/settings). - status: Set to `REQUEST_RELEASE` to request that the session complete. Use before session's timeout to avoid additional charges. + project_id: The Project ID. Can be found in + [Settings](https://www.browserbase.com/settings). + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -510,8 +508,8 @@ async def update( f"/v1/sessions/{id}", body=await async_maybe_transform( { - "project_id": project_id, "status": status, + "project_id": project_id, }, session_update_params.SessionUpdateParams, ), diff --git a/src/browserbase/types/context_create_params.py b/src/browserbase/types/context_create_params.py index 75cd1fcd..66c6c468 100644 --- a/src/browserbase/types/context_create_params.py +++ b/src/browserbase/types/context_create_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,7 +10,7 @@ class ContextCreateParams(TypedDict, total=False): - project_id: Required[Annotated[str, PropertyInfo(alias="projectId")]] + project_id: Annotated[str, PropertyInfo(alias="projectId")] """The Project ID. Can be found in [Settings](https://www.browserbase.com/settings). diff --git a/src/browserbase/types/session_create_params.py b/src/browserbase/types/session_create_params.py index 2ba36400..af594d97 100644 --- a/src/browserbase/types/session_create_params.py +++ b/src/browserbase/types/session_create_params.py @@ -19,16 +19,11 @@ "ProxiesUnionMember0UnionMember0", "ProxiesUnionMember0UnionMember0Geolocation", "ProxiesUnionMember0UnionMember1", + "ProxiesUnionMember0UnionMember2", ] class SessionCreateParams(TypedDict, total=False): - project_id: Required[Annotated[str, PropertyInfo(alias="projectId")]] - """The Project ID. - - Can be found in [Settings](https://www.browserbase.com/settings). - """ - browser_settings: Annotated[BrowserSettings, PropertyInfo(alias="browserSettings")] extension_id: Annotated[str, PropertyInfo(alias="extensionId")] @@ -43,6 +38,12 @@ class SessionCreateParams(TypedDict, total=False): Available on the Hobby Plan and above. """ + project_id: Annotated[str, PropertyInfo(alias="projectId")] + """The Project ID. + + Can be found in [Settings](https://www.browserbase.com/settings). + """ + proxies: Union[Iterable[ProxiesUnionMember0], bool] """Proxy configuration. @@ -213,4 +214,17 @@ class ProxiesUnionMember0UnionMember1(TypedDict, total=False): """Username for external proxy authentication. Optional.""" -ProxiesUnionMember0: TypeAlias = Union[ProxiesUnionMember0UnionMember0, ProxiesUnionMember0UnionMember1] +class ProxiesUnionMember0UnionMember2(TypedDict, total=False): + type: Required[Literal["none"]] + """Type of proxy. Always 'none' for this config.""" + + domain_pattern: Annotated[str, PropertyInfo(alias="domainPattern")] + """Domain pattern for which this proxy should be used. + + If omitted, defaults to all domains. Optional. + """ + + +ProxiesUnionMember0: TypeAlias = Union[ + ProxiesUnionMember0UnionMember0, ProxiesUnionMember0UnionMember1, ProxiesUnionMember0UnionMember2 +] diff --git a/src/browserbase/types/session_update_params.py b/src/browserbase/types/session_update_params.py index 66dcd351..71c589d9 100644 --- a/src/browserbase/types/session_update_params.py +++ b/src/browserbase/types/session_update_params.py @@ -10,14 +10,14 @@ class SessionUpdateParams(TypedDict, total=False): - project_id: Required[Annotated[str, PropertyInfo(alias="projectId")]] - """The Project ID. - - Can be found in [Settings](https://www.browserbase.com/settings). - """ - status: Required[Literal["REQUEST_RELEASE"]] """Set to `REQUEST_RELEASE` to request that the session complete. Use before session's timeout to avoid additional charges. """ + + project_id: Annotated[str, PropertyInfo(alias="projectId")] + """The Project ID. + + Can be found in [Settings](https://www.browserbase.com/settings). + """ diff --git a/tests/api_resources/test_contexts.py b/tests/api_resources/test_contexts.py index 4ad27733..68f50df6 100644 --- a/tests/api_resources/test_contexts.py +++ b/tests/api_resources/test_contexts.py @@ -23,6 +23,11 @@ class TestContexts: @parametrize def test_method_create(self, client: Browserbase) -> None: + context = client.contexts.create() + assert_matches_type(ContextCreateResponse, context, path=["response"]) + + @parametrize + def test_method_create_with_all_params(self, client: Browserbase) -> None: context = client.contexts.create( project_id="projectId", ) @@ -30,9 +35,7 @@ def test_method_create(self, client: Browserbase) -> None: @parametrize def test_raw_response_create(self, client: Browserbase) -> None: - response = client.contexts.with_raw_response.create( - project_id="projectId", - ) + response = client.contexts.with_raw_response.create() assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -41,9 +44,7 @@ def test_raw_response_create(self, client: Browserbase) -> None: @parametrize def test_streaming_response_create(self, client: Browserbase) -> None: - with client.contexts.with_streaming_response.create( - project_id="projectId", - ) as response: + with client.contexts.with_streaming_response.create() as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -136,6 +137,11 @@ class TestAsyncContexts: @parametrize async def test_method_create(self, async_client: AsyncBrowserbase) -> None: + context = await async_client.contexts.create() + assert_matches_type(ContextCreateResponse, context, path=["response"]) + + @parametrize + async def test_method_create_with_all_params(self, async_client: AsyncBrowserbase) -> None: context = await async_client.contexts.create( project_id="projectId", ) @@ -143,9 +149,7 @@ async def test_method_create(self, async_client: AsyncBrowserbase) -> None: @parametrize async def test_raw_response_create(self, async_client: AsyncBrowserbase) -> None: - response = await async_client.contexts.with_raw_response.create( - project_id="projectId", - ) + response = await async_client.contexts.with_raw_response.create() assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -154,9 +158,7 @@ async def test_raw_response_create(self, async_client: AsyncBrowserbase) -> None @parametrize async def test_streaming_response_create(self, async_client: AsyncBrowserbase) -> None: - async with async_client.contexts.with_streaming_response.create( - project_id="projectId", - ) as response: + async with async_client.contexts.with_streaming_response.create() as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" diff --git a/tests/api_resources/test_sessions.py b/tests/api_resources/test_sessions.py index 7a16f64f..7741a3f2 100644 --- a/tests/api_resources/test_sessions.py +++ b/tests/api_resources/test_sessions.py @@ -25,15 +25,12 @@ class TestSessions: @parametrize def test_method_create(self, client: Browserbase) -> None: - session = client.sessions.create( - project_id="projectId", - ) + session = client.sessions.create() assert_matches_type(SessionCreateResponse, session, path=["response"]) @parametrize def test_method_create_with_all_params(self, client: Browserbase) -> None: session = client.sessions.create( - project_id="projectId", browser_settings={ "advanced_stealth": True, "block_ads": True, @@ -68,6 +65,7 @@ def test_method_create_with_all_params(self, client: Browserbase) -> None: }, extension_id="extensionId", keep_alive=True, + project_id="projectId", proxies=[ { "type": "browserbase", @@ -87,9 +85,7 @@ def test_method_create_with_all_params(self, client: Browserbase) -> None: @parametrize def test_raw_response_create(self, client: Browserbase) -> None: - response = client.sessions.with_raw_response.create( - project_id="projectId", - ) + response = client.sessions.with_raw_response.create() assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -98,9 +94,7 @@ def test_raw_response_create(self, client: Browserbase) -> None: @parametrize def test_streaming_response_create(self, client: Browserbase) -> None: - with client.sessions.with_streaming_response.create( - project_id="projectId", - ) as response: + with client.sessions.with_streaming_response.create() as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -151,16 +145,23 @@ def test_path_params_retrieve(self, client: Browserbase) -> None: def test_method_update(self, client: Browserbase) -> None: session = client.sessions.update( id="id", - project_id="projectId", status="REQUEST_RELEASE", ) assert_matches_type(SessionUpdateResponse, session, path=["response"]) + @parametrize + def test_method_update_with_all_params(self, client: Browserbase) -> None: + session = client.sessions.update( + id="id", + status="REQUEST_RELEASE", + project_id="projectId", + ) + assert_matches_type(SessionUpdateResponse, session, path=["response"]) + @parametrize def test_raw_response_update(self, client: Browserbase) -> None: response = client.sessions.with_raw_response.update( id="id", - project_id="projectId", status="REQUEST_RELEASE", ) @@ -173,7 +174,6 @@ def test_raw_response_update(self, client: Browserbase) -> None: def test_streaming_response_update(self, client: Browserbase) -> None: with client.sessions.with_streaming_response.update( id="id", - project_id="projectId", status="REQUEST_RELEASE", ) as response: assert not response.is_closed @@ -189,7 +189,6 @@ def test_path_params_update(self, client: Browserbase) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): client.sessions.with_raw_response.update( id="", - project_id="projectId", status="REQUEST_RELEASE", ) @@ -272,15 +271,12 @@ class TestAsyncSessions: @parametrize async def test_method_create(self, async_client: AsyncBrowserbase) -> None: - session = await async_client.sessions.create( - project_id="projectId", - ) + session = await async_client.sessions.create() assert_matches_type(SessionCreateResponse, session, path=["response"]) @parametrize async def test_method_create_with_all_params(self, async_client: AsyncBrowserbase) -> None: session = await async_client.sessions.create( - project_id="projectId", browser_settings={ "advanced_stealth": True, "block_ads": True, @@ -315,6 +311,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncBrowserbas }, extension_id="extensionId", keep_alive=True, + project_id="projectId", proxies=[ { "type": "browserbase", @@ -334,9 +331,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncBrowserbas @parametrize async def test_raw_response_create(self, async_client: AsyncBrowserbase) -> None: - response = await async_client.sessions.with_raw_response.create( - project_id="projectId", - ) + response = await async_client.sessions.with_raw_response.create() assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -345,9 +340,7 @@ async def test_raw_response_create(self, async_client: AsyncBrowserbase) -> None @parametrize async def test_streaming_response_create(self, async_client: AsyncBrowserbase) -> None: - async with async_client.sessions.with_streaming_response.create( - project_id="projectId", - ) as response: + async with async_client.sessions.with_streaming_response.create() as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -398,16 +391,23 @@ async def test_path_params_retrieve(self, async_client: AsyncBrowserbase) -> Non async def test_method_update(self, async_client: AsyncBrowserbase) -> None: session = await async_client.sessions.update( id="id", - project_id="projectId", status="REQUEST_RELEASE", ) assert_matches_type(SessionUpdateResponse, session, path=["response"]) + @parametrize + async def test_method_update_with_all_params(self, async_client: AsyncBrowserbase) -> None: + session = await async_client.sessions.update( + id="id", + status="REQUEST_RELEASE", + project_id="projectId", + ) + assert_matches_type(SessionUpdateResponse, session, path=["response"]) + @parametrize async def test_raw_response_update(self, async_client: AsyncBrowserbase) -> None: response = await async_client.sessions.with_raw_response.update( id="id", - project_id="projectId", status="REQUEST_RELEASE", ) @@ -420,7 +420,6 @@ async def test_raw_response_update(self, async_client: AsyncBrowserbase) -> None async def test_streaming_response_update(self, async_client: AsyncBrowserbase) -> None: async with async_client.sessions.with_streaming_response.update( id="id", - project_id="projectId", status="REQUEST_RELEASE", ) as response: assert not response.is_closed @@ -436,7 +435,6 @@ async def test_path_params_update(self, async_client: AsyncBrowserbase) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): await async_client.sessions.with_raw_response.update( id="", - project_id="projectId", status="REQUEST_RELEASE", ) diff --git a/tests/test_client.py b/tests/test_client.py index 608cc1f9..71396d22 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -864,7 +864,7 @@ def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter, clien respx_mock.post("/v1/sessions").mock(side_effect=httpx.TimeoutException("Test timeout error")) with pytest.raises(APITimeoutError): - client.sessions.with_streaming_response.create(project_id="projectId").__enter__() + client.sessions.with_streaming_response.create().__enter__() assert _get_open_connections(client) == 0 @@ -874,7 +874,7 @@ def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter, client respx_mock.post("/v1/sessions").mock(return_value=httpx.Response(500)) with pytest.raises(APIStatusError): - client.sessions.with_streaming_response.create(project_id="projectId").__enter__() + client.sessions.with_streaming_response.create().__enter__() assert _get_open_connections(client) == 0 @pytest.mark.parametrize("failures_before_success", [0, 2, 4]) @@ -903,7 +903,7 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: respx_mock.post("/v1/sessions").mock(side_effect=retry_handler) - response = client.sessions.with_raw_response.create(project_id="projectId") + response = client.sessions.with_raw_response.create() assert response.retries_taken == failures_before_success assert int(response.http_request.headers.get("x-stainless-retry-count")) == failures_before_success @@ -927,9 +927,7 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: respx_mock.post("/v1/sessions").mock(side_effect=retry_handler) - response = client.sessions.with_raw_response.create( - project_id="projectId", extra_headers={"x-stainless-retry-count": Omit()} - ) + response = client.sessions.with_raw_response.create(extra_headers={"x-stainless-retry-count": Omit()}) assert len(response.http_request.headers.get_list("x-stainless-retry-count")) == 0 @@ -952,9 +950,7 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: respx_mock.post("/v1/sessions").mock(side_effect=retry_handler) - response = client.sessions.with_raw_response.create( - project_id="projectId", extra_headers={"x-stainless-retry-count": "42"} - ) + response = client.sessions.with_raw_response.create(extra_headers={"x-stainless-retry-count": "42"}) assert response.http_request.headers.get("x-stainless-retry-count") == "42" @@ -1770,7 +1766,7 @@ async def test_retrying_timeout_errors_doesnt_leak( respx_mock.post("/v1/sessions").mock(side_effect=httpx.TimeoutException("Test timeout error")) with pytest.raises(APITimeoutError): - await async_client.sessions.with_streaming_response.create(project_id="projectId").__aenter__() + await async_client.sessions.with_streaming_response.create().__aenter__() assert _get_open_connections(async_client) == 0 @@ -1782,7 +1778,7 @@ async def test_retrying_status_errors_doesnt_leak( respx_mock.post("/v1/sessions").mock(return_value=httpx.Response(500)) with pytest.raises(APIStatusError): - await async_client.sessions.with_streaming_response.create(project_id="projectId").__aenter__() + await async_client.sessions.with_streaming_response.create().__aenter__() assert _get_open_connections(async_client) == 0 @pytest.mark.parametrize("failures_before_success", [0, 2, 4]) @@ -1811,7 +1807,7 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: respx_mock.post("/v1/sessions").mock(side_effect=retry_handler) - response = await client.sessions.with_raw_response.create(project_id="projectId") + response = await client.sessions.with_raw_response.create() assert response.retries_taken == failures_before_success assert int(response.http_request.headers.get("x-stainless-retry-count")) == failures_before_success @@ -1835,9 +1831,7 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: respx_mock.post("/v1/sessions").mock(side_effect=retry_handler) - response = await client.sessions.with_raw_response.create( - project_id="projectId", extra_headers={"x-stainless-retry-count": Omit()} - ) + response = await client.sessions.with_raw_response.create(extra_headers={"x-stainless-retry-count": Omit()}) assert len(response.http_request.headers.get_list("x-stainless-retry-count")) == 0 @@ -1860,9 +1854,7 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: respx_mock.post("/v1/sessions").mock(side_effect=retry_handler) - response = await client.sessions.with_raw_response.create( - project_id="projectId", extra_headers={"x-stainless-retry-count": "42"} - ) + response = await client.sessions.with_raw_response.create(extra_headers={"x-stainless-retry-count": "42"}) assert response.http_request.headers.get("x-stainless-retry-count") == "42" From 9e1ef157c9a792cdbac2217d785e45badee706c1 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 23 Feb 2026 22:42:39 +0000 Subject: [PATCH 239/330] feat(api): manual updates --- .stats.yml | 4 +- api.md | 28 +++--- src/browserbase/resources/contexts.py | 18 ++-- src/browserbase/resources/extensions.py | 27 +++--- src/browserbase/resources/projects.py | 32 +++---- .../resources/sessions/sessions.py | 32 +++---- src/browserbase/types/__init__.py | 13 ++- ...ontext_retrieve_response.py => context.py} | 4 +- ...ension_create_response.py => extension.py} | 4 +- .../types/extension_retrieve_response.py | 22 ----- ...roject_retrieve_response.py => project.py} | 4 +- .../types/project_list_response.py | 27 +----- ...ect_usage_response.py => project_usage.py} | 4 +- ...{session_update_response.py => session.py} | 4 +- .../types/session_create_params.py | 86 +++++-------------- .../types/session_list_response.py | 58 ++----------- ...debug_response.py => session_live_urls.py} | 4 +- src/browserbase/types/sessions/__init__.py | 2 + .../types/sessions/log_list_response.py | 48 +---------- .../sessions/recording_retrieve_response.py | 26 +----- src/browserbase/types/sessions/session_log.py | 46 ++++++++++ .../types/sessions/session_recording.py | 24 ++++++ tests/api_resources/test_contexts.py | 18 ++-- tests/api_resources/test_extensions.py | 26 +++--- tests/api_resources/test_projects.py | 26 +++--- tests/api_resources/test_sessions.py | 82 ++++-------------- 26 files changed, 249 insertions(+), 420 deletions(-) rename src/browserbase/types/{context_retrieve_response.py => context.py} (84%) rename src/browserbase/types/{extension_create_response.py => extension.py} (85%) delete mode 100644 src/browserbase/types/extension_retrieve_response.py rename src/browserbase/types/{project_retrieve_response.py => project.py} (87%) rename src/browserbase/types/{project_usage_response.py => project_usage.py} (78%) rename src/browserbase/types/{session_update_response.py => session.py} (95%) rename src/browserbase/types/{session_debug_response.py => session_live_urls.py} (88%) create mode 100644 src/browserbase/types/sessions/session_log.py create mode 100644 src/browserbase/types/sessions/session_recording.py diff --git a/.stats.yml b/.stats.yml index 766f657d..78eff165 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 18 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fbrowserbase-43f4549d7362ccad24d8f6124e73023a6cd5b62dab9c48cf3173435ca2f23155.yml -openapi_spec_hash: 920add25d34d7a858dad464ea558ce48 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fbrowserbase-b92143ddb16135de4ff65ce8bcdfe9991d11c73570f42f07ea27e0da86209a44.yml +openapi_spec_hash: 16eb6e6c9687f01d2a791775b27dc315 config_hash: b3ca4ec5b02e5333af51ebc2e9fdef1b diff --git a/api.md b/api.md index 01454851..dbb776f6 100644 --- a/api.md +++ b/api.md @@ -3,13 +3,13 @@ Types: ```python -from browserbase.types import ContextCreateResponse, ContextRetrieveResponse, ContextUpdateResponse +from browserbase.types import Context, ContextCreateResponse, ContextUpdateResponse ``` Methods: - client.contexts.create(\*\*params) -> ContextCreateResponse -- client.contexts.retrieve(id) -> ContextRetrieveResponse +- client.contexts.retrieve(id) -> Context - client.contexts.update(id) -> ContextUpdateResponse # Extensions @@ -17,13 +17,13 @@ Methods: Types: ```python -from browserbase.types import ExtensionCreateResponse, ExtensionRetrieveResponse +from browserbase.types import Extension ``` Methods: -- client.extensions.create(\*\*params) -> ExtensionCreateResponse -- client.extensions.retrieve(id) -> ExtensionRetrieveResponse +- client.extensions.create(\*\*params) -> Extension +- client.extensions.retrieve(id) -> Extension - client.extensions.delete(id) -> None # Projects @@ -31,14 +31,14 @@ Methods: Types: ```python -from browserbase.types import ProjectRetrieveResponse, ProjectListResponse, ProjectUsageResponse +from browserbase.types import Project, ProjectUsage, ProjectListResponse ``` Methods: -- client.projects.retrieve(id) -> ProjectRetrieveResponse +- client.projects.retrieve(id) -> Project - client.projects.list() -> ProjectListResponse -- client.projects.usage(id) -> ProjectUsageResponse +- client.projects.usage(id) -> ProjectUsage # Sessions @@ -46,11 +46,11 @@ Types: ```python from browserbase.types import ( + Session, + SessionLiveURLs, SessionCreateResponse, SessionRetrieveResponse, - SessionUpdateResponse, SessionListResponse, - SessionDebugResponse, ) ``` @@ -58,9 +58,9 @@ Methods: - client.sessions.create(\*\*params) -> SessionCreateResponse - client.sessions.retrieve(id) -> SessionRetrieveResponse -- client.sessions.update(id, \*\*params) -> SessionUpdateResponse +- client.sessions.update(id, \*\*params) -> Session - client.sessions.list(\*\*params) -> SessionListResponse -- client.sessions.debug(id) -> SessionDebugResponse +- client.sessions.debug(id) -> SessionLiveURLs ## Downloads @@ -73,7 +73,7 @@ Methods: Types: ```python -from browserbase.types.sessions import LogListResponse +from browserbase.types.sessions import SessionLog, LogListResponse ``` Methods: @@ -85,7 +85,7 @@ Methods: Types: ```python -from browserbase.types.sessions import RecordingRetrieveResponse +from browserbase.types.sessions import SessionRecording, RecordingRetrieveResponse ``` Methods: diff --git a/src/browserbase/resources/contexts.py b/src/browserbase/resources/contexts.py index d7561cf3..4379aa91 100644 --- a/src/browserbase/resources/contexts.py +++ b/src/browserbase/resources/contexts.py @@ -16,9 +16,9 @@ async_to_streamed_response_wrapper, ) from .._base_client import make_request_options +from ..types.context import Context from ..types.context_create_response import ContextCreateResponse from ..types.context_update_response import ContextUpdateResponse -from ..types.context_retrieve_response import ContextRetrieveResponse __all__ = ["ContextsResource", "AsyncContextsResource"] @@ -89,9 +89,9 @@ def retrieve( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> ContextRetrieveResponse: + ) -> Context: """ - Get a Context + Context Args: extra_headers: Send extra headers @@ -109,7 +109,7 @@ def retrieve( options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=ContextRetrieveResponse, + cast_to=Context, ) def update( @@ -124,7 +124,7 @@ def update( timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> ContextUpdateResponse: """ - Update a Context + Update Context Args: extra_headers: Send extra headers @@ -212,9 +212,9 @@ async def retrieve( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> ContextRetrieveResponse: + ) -> Context: """ - Get a Context + Context Args: extra_headers: Send extra headers @@ -232,7 +232,7 @@ async def retrieve( options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=ContextRetrieveResponse, + cast_to=Context, ) async def update( @@ -247,7 +247,7 @@ async def update( timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> ContextUpdateResponse: """ - Update a Context + Update Context Args: extra_headers: Send extra headers diff --git a/src/browserbase/resources/extensions.py b/src/browserbase/resources/extensions.py index 21d06e70..882a495f 100644 --- a/src/browserbase/resources/extensions.py +++ b/src/browserbase/resources/extensions.py @@ -18,8 +18,7 @@ async_to_streamed_response_wrapper, ) from .._base_client import make_request_options -from ..types.extension_create_response import ExtensionCreateResponse -from ..types.extension_retrieve_response import ExtensionRetrieveResponse +from ..types.extension import Extension __all__ = ["ExtensionsResource", "AsyncExtensionsResource"] @@ -54,7 +53,7 @@ def create( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> ExtensionCreateResponse: + ) -> Extension: """ Upload an Extension @@ -80,7 +79,7 @@ def create( options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=ExtensionCreateResponse, + cast_to=Extension, ) def retrieve( @@ -93,9 +92,9 @@ def retrieve( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> ExtensionRetrieveResponse: + ) -> Extension: """ - Get an Extension + Extension Args: extra_headers: Send extra headers @@ -113,7 +112,7 @@ def retrieve( options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=ExtensionRetrieveResponse, + cast_to=Extension, ) def delete( @@ -128,7 +127,7 @@ def delete( timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> None: """ - Delete an Extension + Delete Extension Args: extra_headers: Send extra headers @@ -181,7 +180,7 @@ async def create( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> ExtensionCreateResponse: + ) -> Extension: """ Upload an Extension @@ -207,7 +206,7 @@ async def create( options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=ExtensionCreateResponse, + cast_to=Extension, ) async def retrieve( @@ -220,9 +219,9 @@ async def retrieve( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> ExtensionRetrieveResponse: + ) -> Extension: """ - Get an Extension + Extension Args: extra_headers: Send extra headers @@ -240,7 +239,7 @@ async def retrieve( options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=ExtensionRetrieveResponse, + cast_to=Extension, ) async def delete( @@ -255,7 +254,7 @@ async def delete( timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> None: """ - Delete an Extension + Delete Extension Args: extra_headers: Send extra headers diff --git a/src/browserbase/resources/projects.py b/src/browserbase/resources/projects.py index 62c28afa..a6ae6338 100644 --- a/src/browserbase/resources/projects.py +++ b/src/browserbase/resources/projects.py @@ -14,9 +14,9 @@ async_to_streamed_response_wrapper, ) from .._base_client import make_request_options +from ..types.project import Project +from ..types.project_usage import ProjectUsage from ..types.project_list_response import ProjectListResponse -from ..types.project_usage_response import ProjectUsageResponse -from ..types.project_retrieve_response import ProjectRetrieveResponse __all__ = ["ProjectsResource", "AsyncProjectsResource"] @@ -51,9 +51,9 @@ def retrieve( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> ProjectRetrieveResponse: + ) -> Project: """ - Get a Project + Project Args: extra_headers: Send extra headers @@ -71,7 +71,7 @@ def retrieve( options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=ProjectRetrieveResponse, + cast_to=Project, ) def list( @@ -84,7 +84,7 @@ def list( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> ProjectListResponse: - """List Projects""" + """List projects""" return self._get( "/v1/projects", options=make_request_options( @@ -103,9 +103,9 @@ def usage( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> ProjectUsageResponse: + ) -> ProjectUsage: """ - Get Project Usage + Project Usage Args: extra_headers: Send extra headers @@ -123,7 +123,7 @@ def usage( options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=ProjectUsageResponse, + cast_to=ProjectUsage, ) @@ -157,9 +157,9 @@ async def retrieve( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> ProjectRetrieveResponse: + ) -> Project: """ - Get a Project + Project Args: extra_headers: Send extra headers @@ -177,7 +177,7 @@ async def retrieve( options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=ProjectRetrieveResponse, + cast_to=Project, ) async def list( @@ -190,7 +190,7 @@ async def list( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> ProjectListResponse: - """List Projects""" + """List projects""" return await self._get( "/v1/projects", options=make_request_options( @@ -209,9 +209,9 @@ async def usage( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> ProjectUsageResponse: + ) -> ProjectUsage: """ - Get Project Usage + Project Usage Args: extra_headers: Send extra headers @@ -229,7 +229,7 @@ async def usage( options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=ProjectUsageResponse, + cast_to=ProjectUsage, ) diff --git a/src/browserbase/resources/sessions/sessions.py b/src/browserbase/resources/sessions/sessions.py index 60554305..09fd15a3 100644 --- a/src/browserbase/resources/sessions/sessions.py +++ b/src/browserbase/resources/sessions/sessions.py @@ -51,10 +51,10 @@ async_to_streamed_response_wrapper, ) from ..._base_client import make_request_options +from ...types.session import Session +from ...types.session_live_urls import SessionLiveURLs from ...types.session_list_response import SessionListResponse -from ...types.session_debug_response import SessionDebugResponse from ...types.session_create_response import SessionCreateResponse -from ...types.session_update_response import SessionUpdateResponse from ...types.session_retrieve_response import SessionRetrieveResponse __all__ = ["SessionsResource", "AsyncSessionsResource"] @@ -103,7 +103,7 @@ def create( extension_id: str | Omit = omit, keep_alive: bool | Omit = omit, project_id: str | Omit = omit, - proxies: Union[Iterable[session_create_params.ProxiesUnionMember0], bool] | Omit = omit, + proxies: Union[bool, Iterable[session_create_params.ProxiesUnionMember1]] | Omit = omit, region: Literal["us-west-2", "us-east-1", "eu-central-1", "ap-southeast-1"] | Omit = omit, api_timeout: int | Omit = omit, user_metadata: Dict[str, object] | Omit = omit, @@ -180,7 +180,7 @@ def retrieve( timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> SessionRetrieveResponse: """ - Get a Session + Session Args: extra_headers: Send extra headers @@ -213,9 +213,9 @@ def update( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> SessionUpdateResponse: + ) -> Session: """ - Update a Session + Update Session Args: status: Set to `REQUEST_RELEASE` to request that the session complete. Use before @@ -246,7 +246,7 @@ def update( options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=SessionUpdateResponse, + cast_to=Session, ) def list( @@ -306,7 +306,7 @@ def debug( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> SessionDebugResponse: + ) -> SessionLiveURLs: """ Session Live URLs @@ -326,7 +326,7 @@ def debug( options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=SessionDebugResponse, + cast_to=SessionLiveURLs, ) @@ -373,7 +373,7 @@ async def create( extension_id: str | Omit = omit, keep_alive: bool | Omit = omit, project_id: str | Omit = omit, - proxies: Union[Iterable[session_create_params.ProxiesUnionMember0], bool] | Omit = omit, + proxies: Union[bool, Iterable[session_create_params.ProxiesUnionMember1]] | Omit = omit, region: Literal["us-west-2", "us-east-1", "eu-central-1", "ap-southeast-1"] | Omit = omit, api_timeout: int | Omit = omit, user_metadata: Dict[str, object] | Omit = omit, @@ -450,7 +450,7 @@ async def retrieve( timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> SessionRetrieveResponse: """ - Get a Session + Session Args: extra_headers: Send extra headers @@ -483,9 +483,9 @@ async def update( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> SessionUpdateResponse: + ) -> Session: """ - Update a Session + Update Session Args: status: Set to `REQUEST_RELEASE` to request that the session complete. Use before @@ -516,7 +516,7 @@ async def update( options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=SessionUpdateResponse, + cast_to=Session, ) async def list( @@ -576,7 +576,7 @@ async def debug( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> SessionDebugResponse: + ) -> SessionLiveURLs: """ Session Live URLs @@ -596,7 +596,7 @@ async def debug( options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=SessionDebugResponse, + cast_to=SessionLiveURLs, ) diff --git a/src/browserbase/types/__init__.py b/src/browserbase/types/__init__.py index 20e2f905..4dd85ddb 100644 --- a/src/browserbase/types/__init__.py +++ b/src/browserbase/types/__init__.py @@ -2,21 +2,20 @@ from __future__ import annotations +from .context import Context as Context +from .project import Project as Project +from .session import Session as Session +from .extension import Extension as Extension +from .project_usage import ProjectUsage as ProjectUsage +from .session_live_urls import SessionLiveURLs as SessionLiveURLs from .session_list_params import SessionListParams as SessionListParams from .context_create_params import ContextCreateParams as ContextCreateParams from .project_list_response import ProjectListResponse as ProjectListResponse from .session_create_params import SessionCreateParams as SessionCreateParams from .session_list_response import SessionListResponse as SessionListResponse from .session_update_params import SessionUpdateParams as SessionUpdateParams -from .project_usage_response import ProjectUsageResponse as ProjectUsageResponse -from .session_debug_response import SessionDebugResponse as SessionDebugResponse from .context_create_response import ContextCreateResponse as ContextCreateResponse from .context_update_response import ContextUpdateResponse as ContextUpdateResponse from .extension_create_params import ExtensionCreateParams as ExtensionCreateParams from .session_create_response import SessionCreateResponse as SessionCreateResponse -from .session_update_response import SessionUpdateResponse as SessionUpdateResponse -from .context_retrieve_response import ContextRetrieveResponse as ContextRetrieveResponse -from .extension_create_response import ExtensionCreateResponse as ExtensionCreateResponse -from .project_retrieve_response import ProjectRetrieveResponse as ProjectRetrieveResponse from .session_retrieve_response import SessionRetrieveResponse as SessionRetrieveResponse -from .extension_retrieve_response import ExtensionRetrieveResponse as ExtensionRetrieveResponse diff --git a/src/browserbase/types/context_retrieve_response.py b/src/browserbase/types/context.py similarity index 84% rename from src/browserbase/types/context_retrieve_response.py rename to src/browserbase/types/context.py index c2cd6925..cb5c32fd 100644 --- a/src/browserbase/types/context_retrieve_response.py +++ b/src/browserbase/types/context.py @@ -6,10 +6,10 @@ from .._models import BaseModel -__all__ = ["ContextRetrieveResponse"] +__all__ = ["Context"] -class ContextRetrieveResponse(BaseModel): +class Context(BaseModel): id: str created_at: datetime = FieldInfo(alias="createdAt") diff --git a/src/browserbase/types/extension_create_response.py b/src/browserbase/types/extension.py similarity index 85% rename from src/browserbase/types/extension_create_response.py rename to src/browserbase/types/extension.py index d2b74f41..94582c34 100644 --- a/src/browserbase/types/extension_create_response.py +++ b/src/browserbase/types/extension.py @@ -6,10 +6,10 @@ from .._models import BaseModel -__all__ = ["ExtensionCreateResponse"] +__all__ = ["Extension"] -class ExtensionCreateResponse(BaseModel): +class Extension(BaseModel): id: str created_at: datetime = FieldInfo(alias="createdAt") diff --git a/src/browserbase/types/extension_retrieve_response.py b/src/browserbase/types/extension_retrieve_response.py deleted file mode 100644 index c786348e..00000000 --- a/src/browserbase/types/extension_retrieve_response.py +++ /dev/null @@ -1,22 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from datetime import datetime - -from pydantic import Field as FieldInfo - -from .._models import BaseModel - -__all__ = ["ExtensionRetrieveResponse"] - - -class ExtensionRetrieveResponse(BaseModel): - id: str - - created_at: datetime = FieldInfo(alias="createdAt") - - file_name: str = FieldInfo(alias="fileName") - - project_id: str = FieldInfo(alias="projectId") - """The Project ID linked to the uploaded Extension.""" - - updated_at: datetime = FieldInfo(alias="updatedAt") diff --git a/src/browserbase/types/project_retrieve_response.py b/src/browserbase/types/project.py similarity index 87% rename from src/browserbase/types/project_retrieve_response.py rename to src/browserbase/types/project.py index 78126679..dc3cf335 100644 --- a/src/browserbase/types/project_retrieve_response.py +++ b/src/browserbase/types/project.py @@ -6,10 +6,10 @@ from .._models import BaseModel -__all__ = ["ProjectRetrieveResponse"] +__all__ = ["Project"] -class ProjectRetrieveResponse(BaseModel): +class Project(BaseModel): id: str concurrency: int diff --git a/src/browserbase/types/project_list_response.py b/src/browserbase/types/project_list_response.py index e364b520..2d05a236 100644 --- a/src/browserbase/types/project_list_response.py +++ b/src/browserbase/types/project_list_response.py @@ -1,31 +1,10 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. from typing import List -from datetime import datetime from typing_extensions import TypeAlias -from pydantic import Field as FieldInfo +from .project import Project -from .._models import BaseModel +__all__ = ["ProjectListResponse"] -__all__ = ["ProjectListResponse", "ProjectListResponseItem"] - - -class ProjectListResponseItem(BaseModel): - id: str - - concurrency: int - """The maximum number of sessions that this project can run concurrently.""" - - created_at: datetime = FieldInfo(alias="createdAt") - - default_timeout: int = FieldInfo(alias="defaultTimeout") - - name: str - - owner_id: str = FieldInfo(alias="ownerId") - - updated_at: datetime = FieldInfo(alias="updatedAt") - - -ProjectListResponse: TypeAlias = List[ProjectListResponseItem] +ProjectListResponse: TypeAlias = List[Project] diff --git a/src/browserbase/types/project_usage_response.py b/src/browserbase/types/project_usage.py similarity index 78% rename from src/browserbase/types/project_usage_response.py rename to src/browserbase/types/project_usage.py index b52fccfe..c8a03f5b 100644 --- a/src/browserbase/types/project_usage_response.py +++ b/src/browserbase/types/project_usage.py @@ -4,10 +4,10 @@ from .._models import BaseModel -__all__ = ["ProjectUsageResponse"] +__all__ = ["ProjectUsage"] -class ProjectUsageResponse(BaseModel): +class ProjectUsage(BaseModel): browser_minutes: int = FieldInfo(alias="browserMinutes") proxy_bytes: int = FieldInfo(alias="proxyBytes") diff --git a/src/browserbase/types/session_update_response.py b/src/browserbase/types/session.py similarity index 95% rename from src/browserbase/types/session_update_response.py rename to src/browserbase/types/session.py index 67a13711..16450e29 100644 --- a/src/browserbase/types/session_update_response.py +++ b/src/browserbase/types/session.py @@ -8,10 +8,10 @@ from .._models import BaseModel -__all__ = ["SessionUpdateResponse"] +__all__ = ["Session"] -class SessionUpdateResponse(BaseModel): +class Session(BaseModel): id: str created_at: datetime = FieldInfo(alias="createdAt") diff --git a/src/browserbase/types/session_create_params.py b/src/browserbase/types/session_create_params.py index af594d97..33dd6fd5 100644 --- a/src/browserbase/types/session_create_params.py +++ b/src/browserbase/types/session_create_params.py @@ -2,24 +2,21 @@ from __future__ import annotations -from typing import Dict, List, Union, Iterable +from typing import Dict, Union, Iterable from typing_extensions import Literal, Required, Annotated, TypeAlias, TypedDict -from .._types import SequenceNotStr from .._utils import PropertyInfo __all__ = [ "SessionCreateParams", "BrowserSettings", "BrowserSettingsContext", - "BrowserSettingsFingerprint", - "BrowserSettingsFingerprintScreen", "BrowserSettingsViewport", - "ProxiesUnionMember0", - "ProxiesUnionMember0UnionMember0", - "ProxiesUnionMember0UnionMember0Geolocation", - "ProxiesUnionMember0UnionMember1", - "ProxiesUnionMember0UnionMember2", + "ProxiesUnionMember1", + "ProxiesUnionMember1BrowserbaseProxyConfig", + "ProxiesUnionMember1BrowserbaseProxyConfigGeolocation", + "ProxiesUnionMember1ExternalProxyConfig", + "ProxiesUnionMember1NoneProxyConfig", ] @@ -44,7 +41,7 @@ class SessionCreateParams(TypedDict, total=False): Can be found in [Settings](https://www.browserbase.com/settings). """ - proxies: Union[Iterable[ProxiesUnionMember0], bool] + proxies: Union[bool, Iterable[ProxiesUnionMember1]] """Proxy configuration. Can be true for default proxy, or an array of proxy configurations. @@ -75,42 +72,10 @@ class BrowserSettingsContext(TypedDict, total=False): """Whether or not to persist the context after browsing. Defaults to `false`.""" -class BrowserSettingsFingerprintScreen(TypedDict, total=False): - max_height: Annotated[int, PropertyInfo(alias="maxHeight")] - - max_width: Annotated[int, PropertyInfo(alias="maxWidth")] - - min_height: Annotated[int, PropertyInfo(alias="minHeight")] - - min_width: Annotated[int, PropertyInfo(alias="minWidth")] - - -class BrowserSettingsFingerprint(TypedDict, total=False): - """ - See usage examples [on the Stealth Mode page](/features/stealth-mode#fingerprinting) - """ - - 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: BrowserSettingsFingerprintScreen - - class BrowserSettingsViewport(TypedDict, total=False): height: int - """The height of the browser.""" width: int - """The width of the browser.""" class BrowserSettings(TypedDict, total=False): @@ -140,12 +105,6 @@ class BrowserSettings(TypedDict, total=False): See [Upload Extension](/reference/api/upload-an-extension). """ - fingerprint: BrowserSettingsFingerprint - """ - See usage examples - [on the Stealth Mode page](/features/stealth-mode#fingerprinting) - """ - log_session: Annotated[bool, PropertyInfo(alias="logSession")] """Enable or disable session logging. Defaults to `true`.""" @@ -164,8 +123,8 @@ class BrowserSettings(TypedDict, total=False): viewport: BrowserSettingsViewport -class ProxiesUnionMember0UnionMember0Geolocation(TypedDict, total=False): - """Geographic location for the proxy. Optional.""" +class ProxiesUnionMember1BrowserbaseProxyConfigGeolocation(TypedDict, total=False): + """Configuration for geolocation""" country: Required[str] """Country code in ISO 3166-1 alpha-2 format""" @@ -177,7 +136,7 @@ class ProxiesUnionMember0UnionMember0Geolocation(TypedDict, total=False): """US state code (2 characters). Must also specify US as the country. Optional.""" -class ProxiesUnionMember0UnionMember0(TypedDict, total=False): +class ProxiesUnionMember1BrowserbaseProxyConfig(TypedDict, total=False): type: Required[Literal["browserbase"]] """Type of proxy. @@ -190,11 +149,11 @@ class ProxiesUnionMember0UnionMember0(TypedDict, total=False): If omitted, defaults to all domains. Optional. """ - geolocation: ProxiesUnionMember0UnionMember0Geolocation - """Geographic location for the proxy. Optional.""" + geolocation: ProxiesUnionMember1BrowserbaseProxyConfigGeolocation + """Configuration for geolocation""" -class ProxiesUnionMember0UnionMember1(TypedDict, total=False): +class ProxiesUnionMember1ExternalProxyConfig(TypedDict, total=False): server: Required[str] """Server URL for external proxy. Required.""" @@ -214,17 +173,16 @@ class ProxiesUnionMember0UnionMember1(TypedDict, total=False): """Username for external proxy authentication. Optional.""" -class ProxiesUnionMember0UnionMember2(TypedDict, total=False): - type: Required[Literal["none"]] - """Type of proxy. Always 'none' for this config.""" - - domain_pattern: Annotated[str, PropertyInfo(alias="domainPattern")] - """Domain pattern for which this proxy should be used. +class ProxiesUnionMember1NoneProxyConfig(TypedDict, total=False): + domain_pattern: Required[Annotated[str, PropertyInfo(alias="domainPattern")]] + """Domain pattern for which site should have proxies disabled.""" - If omitted, defaults to all domains. Optional. - """ + type: Required[Literal["none"]] + """Type of proxy. Use 'none' to disable proxy for matching domains.""" -ProxiesUnionMember0: TypeAlias = Union[ - ProxiesUnionMember0UnionMember0, ProxiesUnionMember0UnionMember1, ProxiesUnionMember0UnionMember2 +ProxiesUnionMember1: TypeAlias = Union[ + ProxiesUnionMember1BrowserbaseProxyConfig, + ProxiesUnionMember1ExternalProxyConfig, + ProxiesUnionMember1NoneProxyConfig, ] diff --git a/src/browserbase/types/session_list_response.py b/src/browserbase/types/session_list_response.py index 4c1bd885..ca162ddb 100644 --- a/src/browserbase/types/session_list_response.py +++ b/src/browserbase/types/session_list_response.py @@ -1,58 +1,10 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -from typing import Dict, List, Optional -from datetime import datetime -from typing_extensions import Literal, TypeAlias +from typing import List +from typing_extensions import TypeAlias -from pydantic import Field as FieldInfo +from .session import Session -from .._models import BaseModel +__all__ = ["SessionListResponse"] -__all__ = ["SessionListResponse", "SessionListResponseItem"] - - -class SessionListResponseItem(BaseModel): - id: str - - created_at: datetime = FieldInfo(alias="createdAt") - - expires_at: datetime = FieldInfo(alias="expiresAt") - - keep_alive: bool = FieldInfo(alias="keepAlive") - """Indicates if the Session was created to be kept alive upon disconnections""" - - project_id: str = FieldInfo(alias="projectId") - """The Project ID linked to the Session.""" - - proxy_bytes: int = FieldInfo(alias="proxyBytes") - """Bytes used via the [Proxy](/features/stealth-mode#proxies-and-residential-ips)""" - - region: Literal["us-west-2", "us-east-1", "eu-central-1", "ap-southeast-1"] - """The region where the Session is running.""" - - started_at: datetime = FieldInfo(alias="startedAt") - - status: Literal["RUNNING", "ERROR", "TIMED_OUT", "COMPLETED"] - - updated_at: datetime = FieldInfo(alias="updatedAt") - - avg_cpu_usage: Optional[int] = FieldInfo(alias="avgCpuUsage", default=None) - """CPU used by the Session""" - - context_id: Optional[str] = FieldInfo(alias="contextId", default=None) - """Optional. The Context linked to the Session.""" - - ended_at: Optional[datetime] = FieldInfo(alias="endedAt", default=None) - - memory_usage: Optional[int] = FieldInfo(alias="memoryUsage", default=None) - """Memory used by the Session""" - - user_metadata: Optional[Dict[str, object]] = FieldInfo(alias="userMetadata", default=None) - """Arbitrary user metadata to attach to the session. - - To learn more about user metadata, see - [User Metadata](/features/sessions#user-metadata). - """ - - -SessionListResponse: TypeAlias = List[SessionListResponseItem] +SessionListResponse: TypeAlias = List[Session] diff --git a/src/browserbase/types/session_debug_response.py b/src/browserbase/types/session_live_urls.py similarity index 88% rename from src/browserbase/types/session_debug_response.py rename to src/browserbase/types/session_live_urls.py index 9cee7a77..3c7ba320 100644 --- a/src/browserbase/types/session_debug_response.py +++ b/src/browserbase/types/session_live_urls.py @@ -6,7 +6,7 @@ from .._models import BaseModel -__all__ = ["SessionDebugResponse", "Page"] +__all__ = ["SessionLiveURLs", "Page"] class Page(BaseModel): @@ -23,7 +23,7 @@ class Page(BaseModel): url: str -class SessionDebugResponse(BaseModel): +class SessionLiveURLs(BaseModel): debugger_fullscreen_url: str = FieldInfo(alias="debuggerFullscreenUrl") debugger_url: str = FieldInfo(alias="debuggerUrl") diff --git a/src/browserbase/types/sessions/__init__.py b/src/browserbase/types/sessions/__init__.py index 69d54703..0cef6b19 100644 --- a/src/browserbase/types/sessions/__init__.py +++ b/src/browserbase/types/sessions/__init__.py @@ -2,7 +2,9 @@ from __future__ import annotations +from .session_log import SessionLog as SessionLog from .log_list_response import LogListResponse as LogListResponse +from .session_recording import SessionRecording as SessionRecording from .upload_create_params import UploadCreateParams as UploadCreateParams from .upload_create_response import UploadCreateResponse as UploadCreateResponse from .recording_retrieve_response import RecordingRetrieveResponse as RecordingRetrieveResponse diff --git a/src/browserbase/types/sessions/log_list_response.py b/src/browserbase/types/sessions/log_list_response.py index efd848ab..2b325a8c 100644 --- a/src/browserbase/types/sessions/log_list_response.py +++ b/src/browserbase/types/sessions/log_list_response.py @@ -1,50 +1,10 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -from typing import Dict, List, Optional +from typing import List from typing_extensions import TypeAlias -from pydantic import Field as FieldInfo +from .session_log import SessionLog -from ..._models import BaseModel +__all__ = ["LogListResponse"] -__all__ = ["LogListResponse", "LogListResponseItem", "LogListResponseItemRequest", "LogListResponseItemResponse"] - - -class LogListResponseItemRequest(BaseModel): - params: Dict[str, object] - - raw_body: str = FieldInfo(alias="rawBody") - - timestamp: Optional[int] = None - """milliseconds that have elapsed since the UNIX epoch""" - - -class LogListResponseItemResponse(BaseModel): - raw_body: str = FieldInfo(alias="rawBody") - - result: Dict[str, object] - - timestamp: Optional[int] = None - """milliseconds that have elapsed since the UNIX epoch""" - - -class LogListResponseItem(BaseModel): - method: str - - page_id: int = FieldInfo(alias="pageId") - - session_id: str = FieldInfo(alias="sessionId") - - frame_id: Optional[str] = FieldInfo(alias="frameId", default=None) - - loader_id: Optional[str] = FieldInfo(alias="loaderId", default=None) - - request: Optional[LogListResponseItemRequest] = None - - response: Optional[LogListResponseItemResponse] = None - - timestamp: Optional[int] = None - """milliseconds that have elapsed since the UNIX epoch""" - - -LogListResponse: TypeAlias = List[LogListResponseItem] +LogListResponse: TypeAlias = List[SessionLog] diff --git a/src/browserbase/types/sessions/recording_retrieve_response.py b/src/browserbase/types/sessions/recording_retrieve_response.py index d3613b8c..951969bb 100644 --- a/src/browserbase/types/sessions/recording_retrieve_response.py +++ b/src/browserbase/types/sessions/recording_retrieve_response.py @@ -1,28 +1,10 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -from typing import Dict, List +from typing import List from typing_extensions import TypeAlias -from pydantic import Field as FieldInfo +from .session_recording import SessionRecording -from ..._models import BaseModel +__all__ = ["RecordingRetrieveResponse"] -__all__ = ["RecordingRetrieveResponse", "RecordingRetrieveResponseItem"] - - -class RecordingRetrieveResponseItem(BaseModel): - data: Dict[str, object] - """ - See - [rrweb documentation](https://github.com/rrweb-io/rrweb/blob/master/docs/recipes/dive-into-event.md). - """ - - session_id: str = FieldInfo(alias="sessionId") - - timestamp: int - """milliseconds that have elapsed since the UNIX epoch""" - - type: int - - -RecordingRetrieveResponse: TypeAlias = List[RecordingRetrieveResponseItem] +RecordingRetrieveResponse: TypeAlias = List[SessionRecording] diff --git a/src/browserbase/types/sessions/session_log.py b/src/browserbase/types/sessions/session_log.py new file mode 100644 index 00000000..428f518a --- /dev/null +++ b/src/browserbase/types/sessions/session_log.py @@ -0,0 +1,46 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Dict, Optional + +from pydantic import Field as FieldInfo + +from ..._models import BaseModel + +__all__ = ["SessionLog", "Request", "Response"] + + +class Request(BaseModel): + params: Dict[str, object] + + raw_body: str = FieldInfo(alias="rawBody") + + timestamp: Optional[int] = None + """milliseconds that have elapsed since the UNIX epoch""" + + +class Response(BaseModel): + raw_body: str = FieldInfo(alias="rawBody") + + result: Dict[str, object] + + timestamp: Optional[int] = None + """milliseconds that have elapsed since the UNIX epoch""" + + +class SessionLog(BaseModel): + method: str + + page_id: int = FieldInfo(alias="pageId") + + session_id: str = FieldInfo(alias="sessionId") + + frame_id: Optional[str] = FieldInfo(alias="frameId", default=None) + + loader_id: Optional[str] = FieldInfo(alias="loaderId", default=None) + + request: Optional[Request] = None + + response: Optional[Response] = None + + timestamp: Optional[int] = None + """milliseconds that have elapsed since the UNIX epoch""" diff --git a/src/browserbase/types/sessions/session_recording.py b/src/browserbase/types/sessions/session_recording.py new file mode 100644 index 00000000..c8471371 --- /dev/null +++ b/src/browserbase/types/sessions/session_recording.py @@ -0,0 +1,24 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Dict + +from pydantic import Field as FieldInfo + +from ..._models import BaseModel + +__all__ = ["SessionRecording"] + + +class SessionRecording(BaseModel): + data: Dict[str, object] + """ + See + [rrweb documentation](https://github.com/rrweb-io/rrweb/blob/master/docs/recipes/dive-into-event.md). + """ + + session_id: str = FieldInfo(alias="sessionId") + + timestamp: int + """milliseconds that have elapsed since the UNIX epoch""" + + type: int diff --git a/tests/api_resources/test_contexts.py b/tests/api_resources/test_contexts.py index 68f50df6..83267ca7 100644 --- a/tests/api_resources/test_contexts.py +++ b/tests/api_resources/test_contexts.py @@ -9,11 +9,7 @@ from browserbase import Browserbase, AsyncBrowserbase from tests.utils import assert_matches_type -from browserbase.types import ( - ContextCreateResponse, - ContextUpdateResponse, - ContextRetrieveResponse, -) +from browserbase.types import Context, ContextCreateResponse, ContextUpdateResponse base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") @@ -58,7 +54,7 @@ def test_method_retrieve(self, client: Browserbase) -> None: context = client.contexts.retrieve( "id", ) - assert_matches_type(ContextRetrieveResponse, context, path=["response"]) + assert_matches_type(Context, context, path=["response"]) @parametrize def test_raw_response_retrieve(self, client: Browserbase) -> None: @@ -69,7 +65,7 @@ def test_raw_response_retrieve(self, client: Browserbase) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" context = response.parse() - assert_matches_type(ContextRetrieveResponse, context, path=["response"]) + assert_matches_type(Context, context, path=["response"]) @parametrize def test_streaming_response_retrieve(self, client: Browserbase) -> None: @@ -80,7 +76,7 @@ def test_streaming_response_retrieve(self, client: Browserbase) -> None: assert response.http_request.headers.get("X-Stainless-Lang") == "python" context = response.parse() - assert_matches_type(ContextRetrieveResponse, context, path=["response"]) + assert_matches_type(Context, context, path=["response"]) assert cast(Any, response.is_closed) is True @@ -172,7 +168,7 @@ async def test_method_retrieve(self, async_client: AsyncBrowserbase) -> None: context = await async_client.contexts.retrieve( "id", ) - assert_matches_type(ContextRetrieveResponse, context, path=["response"]) + assert_matches_type(Context, context, path=["response"]) @parametrize async def test_raw_response_retrieve(self, async_client: AsyncBrowserbase) -> None: @@ -183,7 +179,7 @@ async def test_raw_response_retrieve(self, async_client: AsyncBrowserbase) -> No assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" context = await response.parse() - assert_matches_type(ContextRetrieveResponse, context, path=["response"]) + assert_matches_type(Context, context, path=["response"]) @parametrize async def test_streaming_response_retrieve(self, async_client: AsyncBrowserbase) -> None: @@ -194,7 +190,7 @@ async def test_streaming_response_retrieve(self, async_client: AsyncBrowserbase) assert response.http_request.headers.get("X-Stainless-Lang") == "python" context = await response.parse() - assert_matches_type(ContextRetrieveResponse, context, path=["response"]) + assert_matches_type(Context, context, path=["response"]) assert cast(Any, response.is_closed) is True diff --git a/tests/api_resources/test_extensions.py b/tests/api_resources/test_extensions.py index e32ae9b0..6b6a0183 100644 --- a/tests/api_resources/test_extensions.py +++ b/tests/api_resources/test_extensions.py @@ -9,7 +9,7 @@ from browserbase import Browserbase, AsyncBrowserbase from tests.utils import assert_matches_type -from browserbase.types import ExtensionCreateResponse, ExtensionRetrieveResponse +from browserbase.types import Extension base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") @@ -22,7 +22,7 @@ def test_method_create(self, client: Browserbase) -> None: extension = client.extensions.create( file=b"raw file contents", ) - assert_matches_type(ExtensionCreateResponse, extension, path=["response"]) + assert_matches_type(Extension, extension, path=["response"]) @parametrize def test_raw_response_create(self, client: Browserbase) -> None: @@ -33,7 +33,7 @@ def test_raw_response_create(self, client: Browserbase) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" extension = response.parse() - assert_matches_type(ExtensionCreateResponse, extension, path=["response"]) + assert_matches_type(Extension, extension, path=["response"]) @parametrize def test_streaming_response_create(self, client: Browserbase) -> None: @@ -44,7 +44,7 @@ def test_streaming_response_create(self, client: Browserbase) -> None: assert response.http_request.headers.get("X-Stainless-Lang") == "python" extension = response.parse() - assert_matches_type(ExtensionCreateResponse, extension, path=["response"]) + assert_matches_type(Extension, extension, path=["response"]) assert cast(Any, response.is_closed) is True @@ -53,7 +53,7 @@ def test_method_retrieve(self, client: Browserbase) -> None: extension = client.extensions.retrieve( "id", ) - assert_matches_type(ExtensionRetrieveResponse, extension, path=["response"]) + assert_matches_type(Extension, extension, path=["response"]) @parametrize def test_raw_response_retrieve(self, client: Browserbase) -> None: @@ -64,7 +64,7 @@ def test_raw_response_retrieve(self, client: Browserbase) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" extension = response.parse() - assert_matches_type(ExtensionRetrieveResponse, extension, path=["response"]) + assert_matches_type(Extension, extension, path=["response"]) @parametrize def test_streaming_response_retrieve(self, client: Browserbase) -> None: @@ -75,7 +75,7 @@ def test_streaming_response_retrieve(self, client: Browserbase) -> None: assert response.http_request.headers.get("X-Stainless-Lang") == "python" extension = response.parse() - assert_matches_type(ExtensionRetrieveResponse, extension, path=["response"]) + assert_matches_type(Extension, extension, path=["response"]) assert cast(Any, response.is_closed) is True @@ -135,7 +135,7 @@ async def test_method_create(self, async_client: AsyncBrowserbase) -> None: extension = await async_client.extensions.create( file=b"raw file contents", ) - assert_matches_type(ExtensionCreateResponse, extension, path=["response"]) + assert_matches_type(Extension, extension, path=["response"]) @parametrize async def test_raw_response_create(self, async_client: AsyncBrowserbase) -> None: @@ -146,7 +146,7 @@ async def test_raw_response_create(self, async_client: AsyncBrowserbase) -> None assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" extension = await response.parse() - assert_matches_type(ExtensionCreateResponse, extension, path=["response"]) + assert_matches_type(Extension, extension, path=["response"]) @parametrize async def test_streaming_response_create(self, async_client: AsyncBrowserbase) -> None: @@ -157,7 +157,7 @@ async def test_streaming_response_create(self, async_client: AsyncBrowserbase) - assert response.http_request.headers.get("X-Stainless-Lang") == "python" extension = await response.parse() - assert_matches_type(ExtensionCreateResponse, extension, path=["response"]) + assert_matches_type(Extension, extension, path=["response"]) assert cast(Any, response.is_closed) is True @@ -166,7 +166,7 @@ async def test_method_retrieve(self, async_client: AsyncBrowserbase) -> None: extension = await async_client.extensions.retrieve( "id", ) - assert_matches_type(ExtensionRetrieveResponse, extension, path=["response"]) + assert_matches_type(Extension, extension, path=["response"]) @parametrize async def test_raw_response_retrieve(self, async_client: AsyncBrowserbase) -> None: @@ -177,7 +177,7 @@ async def test_raw_response_retrieve(self, async_client: AsyncBrowserbase) -> No assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" extension = await response.parse() - assert_matches_type(ExtensionRetrieveResponse, extension, path=["response"]) + assert_matches_type(Extension, extension, path=["response"]) @parametrize async def test_streaming_response_retrieve(self, async_client: AsyncBrowserbase) -> None: @@ -188,7 +188,7 @@ async def test_streaming_response_retrieve(self, async_client: AsyncBrowserbase) assert response.http_request.headers.get("X-Stainless-Lang") == "python" extension = await response.parse() - assert_matches_type(ExtensionRetrieveResponse, extension, path=["response"]) + assert_matches_type(Extension, extension, path=["response"]) assert cast(Any, response.is_closed) is True diff --git a/tests/api_resources/test_projects.py b/tests/api_resources/test_projects.py index 0d8e3c94..c8241bf8 100644 --- a/tests/api_resources/test_projects.py +++ b/tests/api_resources/test_projects.py @@ -9,7 +9,7 @@ from browserbase import Browserbase, AsyncBrowserbase from tests.utils import assert_matches_type -from browserbase.types import ProjectListResponse, ProjectUsageResponse, ProjectRetrieveResponse +from browserbase.types import Project, ProjectUsage, ProjectListResponse base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") @@ -22,7 +22,7 @@ def test_method_retrieve(self, client: Browserbase) -> None: project = client.projects.retrieve( "id", ) - assert_matches_type(ProjectRetrieveResponse, project, path=["response"]) + assert_matches_type(Project, project, path=["response"]) @parametrize def test_raw_response_retrieve(self, client: Browserbase) -> None: @@ -33,7 +33,7 @@ def test_raw_response_retrieve(self, client: Browserbase) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" project = response.parse() - assert_matches_type(ProjectRetrieveResponse, project, path=["response"]) + assert_matches_type(Project, project, path=["response"]) @parametrize def test_streaming_response_retrieve(self, client: Browserbase) -> None: @@ -44,7 +44,7 @@ def test_streaming_response_retrieve(self, client: Browserbase) -> None: assert response.http_request.headers.get("X-Stainless-Lang") == "python" project = response.parse() - assert_matches_type(ProjectRetrieveResponse, project, path=["response"]) + assert_matches_type(Project, project, path=["response"]) assert cast(Any, response.is_closed) is True @@ -85,7 +85,7 @@ def test_method_usage(self, client: Browserbase) -> None: project = client.projects.usage( "id", ) - assert_matches_type(ProjectUsageResponse, project, path=["response"]) + assert_matches_type(ProjectUsage, project, path=["response"]) @parametrize def test_raw_response_usage(self, client: Browserbase) -> None: @@ -96,7 +96,7 @@ def test_raw_response_usage(self, client: Browserbase) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" project = response.parse() - assert_matches_type(ProjectUsageResponse, project, path=["response"]) + assert_matches_type(ProjectUsage, project, path=["response"]) @parametrize def test_streaming_response_usage(self, client: Browserbase) -> None: @@ -107,7 +107,7 @@ def test_streaming_response_usage(self, client: Browserbase) -> None: assert response.http_request.headers.get("X-Stainless-Lang") == "python" project = response.parse() - assert_matches_type(ProjectUsageResponse, project, path=["response"]) + assert_matches_type(ProjectUsage, project, path=["response"]) assert cast(Any, response.is_closed) is True @@ -129,7 +129,7 @@ async def test_method_retrieve(self, async_client: AsyncBrowserbase) -> None: project = await async_client.projects.retrieve( "id", ) - assert_matches_type(ProjectRetrieveResponse, project, path=["response"]) + assert_matches_type(Project, project, path=["response"]) @parametrize async def test_raw_response_retrieve(self, async_client: AsyncBrowserbase) -> None: @@ -140,7 +140,7 @@ async def test_raw_response_retrieve(self, async_client: AsyncBrowserbase) -> No assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" project = await response.parse() - assert_matches_type(ProjectRetrieveResponse, project, path=["response"]) + assert_matches_type(Project, project, path=["response"]) @parametrize async def test_streaming_response_retrieve(self, async_client: AsyncBrowserbase) -> None: @@ -151,7 +151,7 @@ async def test_streaming_response_retrieve(self, async_client: AsyncBrowserbase) assert response.http_request.headers.get("X-Stainless-Lang") == "python" project = await response.parse() - assert_matches_type(ProjectRetrieveResponse, project, path=["response"]) + assert_matches_type(Project, project, path=["response"]) assert cast(Any, response.is_closed) is True @@ -192,7 +192,7 @@ async def test_method_usage(self, async_client: AsyncBrowserbase) -> None: project = await async_client.projects.usage( "id", ) - assert_matches_type(ProjectUsageResponse, project, path=["response"]) + assert_matches_type(ProjectUsage, project, path=["response"]) @parametrize async def test_raw_response_usage(self, async_client: AsyncBrowserbase) -> None: @@ -203,7 +203,7 @@ async def test_raw_response_usage(self, async_client: AsyncBrowserbase) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" project = await response.parse() - assert_matches_type(ProjectUsageResponse, project, path=["response"]) + assert_matches_type(ProjectUsage, project, path=["response"]) @parametrize async def test_streaming_response_usage(self, async_client: AsyncBrowserbase) -> None: @@ -214,7 +214,7 @@ async def test_streaming_response_usage(self, async_client: AsyncBrowserbase) -> assert response.http_request.headers.get("X-Stainless-Lang") == "python" project = await response.parse() - assert_matches_type(ProjectUsageResponse, project, path=["response"]) + assert_matches_type(ProjectUsage, project, path=["response"]) assert cast(Any, response.is_closed) is True diff --git a/tests/api_resources/test_sessions.py b/tests/api_resources/test_sessions.py index 7741a3f2..6a21aa85 100644 --- a/tests/api_resources/test_sessions.py +++ b/tests/api_resources/test_sessions.py @@ -10,10 +10,10 @@ from browserbase import Browserbase, AsyncBrowserbase from tests.utils import assert_matches_type from browserbase.types import ( + Session, + SessionLiveURLs, SessionListResponse, - SessionDebugResponse, SessionCreateResponse, - SessionUpdateResponse, SessionRetrieveResponse, ) @@ -41,19 +41,6 @@ def test_method_create_with_all_params(self, client: Browserbase) -> None: "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, "os": "windows", "record_session": True, @@ -66,17 +53,7 @@ def test_method_create_with_all_params(self, client: Browserbase) -> None: extension_id="extensionId", keep_alive=True, project_id="projectId", - proxies=[ - { - "type": "browserbase", - "domain_pattern": "domainPattern", - "geolocation": { - "country": "xx", - "city": "city", - "state": "xx", - }, - } - ], + proxies=True, region="us-west-2", api_timeout=60, user_metadata={"foo": "bar"}, @@ -147,7 +124,7 @@ def test_method_update(self, client: Browserbase) -> None: id="id", status="REQUEST_RELEASE", ) - assert_matches_type(SessionUpdateResponse, session, path=["response"]) + assert_matches_type(Session, session, path=["response"]) @parametrize def test_method_update_with_all_params(self, client: Browserbase) -> None: @@ -156,7 +133,7 @@ def test_method_update_with_all_params(self, client: Browserbase) -> None: status="REQUEST_RELEASE", project_id="projectId", ) - assert_matches_type(SessionUpdateResponse, session, path=["response"]) + assert_matches_type(Session, session, path=["response"]) @parametrize def test_raw_response_update(self, client: Browserbase) -> None: @@ -168,7 +145,7 @@ def test_raw_response_update(self, client: Browserbase) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" session = response.parse() - assert_matches_type(SessionUpdateResponse, session, path=["response"]) + assert_matches_type(Session, session, path=["response"]) @parametrize def test_streaming_response_update(self, client: Browserbase) -> None: @@ -180,7 +157,7 @@ def test_streaming_response_update(self, client: Browserbase) -> None: assert response.http_request.headers.get("X-Stainless-Lang") == "python" session = response.parse() - assert_matches_type(SessionUpdateResponse, session, path=["response"]) + assert_matches_type(Session, session, path=["response"]) assert cast(Any, response.is_closed) is True @@ -230,7 +207,7 @@ def test_method_debug(self, client: Browserbase) -> None: session = client.sessions.debug( "id", ) - assert_matches_type(SessionDebugResponse, session, path=["response"]) + assert_matches_type(SessionLiveURLs, session, path=["response"]) @parametrize def test_raw_response_debug(self, client: Browserbase) -> None: @@ -241,7 +218,7 @@ def test_raw_response_debug(self, client: Browserbase) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" session = response.parse() - assert_matches_type(SessionDebugResponse, session, path=["response"]) + assert_matches_type(SessionLiveURLs, session, path=["response"]) @parametrize def test_streaming_response_debug(self, client: Browserbase) -> None: @@ -252,7 +229,7 @@ def test_streaming_response_debug(self, client: Browserbase) -> None: assert response.http_request.headers.get("X-Stainless-Lang") == "python" session = response.parse() - assert_matches_type(SessionDebugResponse, session, path=["response"]) + assert_matches_type(SessionLiveURLs, session, path=["response"]) assert cast(Any, response.is_closed) is True @@ -287,19 +264,6 @@ async def test_method_create_with_all_params(self, async_client: AsyncBrowserbas "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, "os": "windows", "record_session": True, @@ -312,17 +276,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncBrowserbas extension_id="extensionId", keep_alive=True, project_id="projectId", - proxies=[ - { - "type": "browserbase", - "domain_pattern": "domainPattern", - "geolocation": { - "country": "xx", - "city": "city", - "state": "xx", - }, - } - ], + proxies=True, region="us-west-2", api_timeout=60, user_metadata={"foo": "bar"}, @@ -393,7 +347,7 @@ async def test_method_update(self, async_client: AsyncBrowserbase) -> None: id="id", status="REQUEST_RELEASE", ) - assert_matches_type(SessionUpdateResponse, session, path=["response"]) + assert_matches_type(Session, session, path=["response"]) @parametrize async def test_method_update_with_all_params(self, async_client: AsyncBrowserbase) -> None: @@ -402,7 +356,7 @@ async def test_method_update_with_all_params(self, async_client: AsyncBrowserbas status="REQUEST_RELEASE", project_id="projectId", ) - assert_matches_type(SessionUpdateResponse, session, path=["response"]) + assert_matches_type(Session, session, path=["response"]) @parametrize async def test_raw_response_update(self, async_client: AsyncBrowserbase) -> None: @@ -414,7 +368,7 @@ async def test_raw_response_update(self, async_client: AsyncBrowserbase) -> None assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" session = await response.parse() - assert_matches_type(SessionUpdateResponse, session, path=["response"]) + assert_matches_type(Session, session, path=["response"]) @parametrize async def test_streaming_response_update(self, async_client: AsyncBrowserbase) -> None: @@ -426,7 +380,7 @@ async def test_streaming_response_update(self, async_client: AsyncBrowserbase) - assert response.http_request.headers.get("X-Stainless-Lang") == "python" session = await response.parse() - assert_matches_type(SessionUpdateResponse, session, path=["response"]) + assert_matches_type(Session, session, path=["response"]) assert cast(Any, response.is_closed) is True @@ -476,7 +430,7 @@ async def test_method_debug(self, async_client: AsyncBrowserbase) -> None: session = await async_client.sessions.debug( "id", ) - assert_matches_type(SessionDebugResponse, session, path=["response"]) + assert_matches_type(SessionLiveURLs, session, path=["response"]) @parametrize async def test_raw_response_debug(self, async_client: AsyncBrowserbase) -> None: @@ -487,7 +441,7 @@ async def test_raw_response_debug(self, async_client: AsyncBrowserbase) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" session = await response.parse() - assert_matches_type(SessionDebugResponse, session, path=["response"]) + assert_matches_type(SessionLiveURLs, session, path=["response"]) @parametrize async def test_streaming_response_debug(self, async_client: AsyncBrowserbase) -> None: @@ -498,7 +452,7 @@ async def test_streaming_response_debug(self, async_client: AsyncBrowserbase) -> assert response.http_request.headers.get("X-Stainless-Lang") == "python" session = await response.parse() - assert_matches_type(SessionDebugResponse, session, path=["response"]) + assert_matches_type(SessionLiveURLs, session, path=["response"]) assert cast(Any, response.is_closed) is True From 8f633a1aa0109a42a50fedfbfff7953381fb9a45 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 23 Feb 2026 22:46:08 +0000 Subject: [PATCH 240/330] feat(api): manual updates --- .stats.yml | 4 +- api.md | 1 + src/browserbase/resources/contexts.py | 82 ++++++++++++++++++++++++++- tests/api_resources/test_contexts.py | 76 +++++++++++++++++++++++++ 4 files changed, 160 insertions(+), 3 deletions(-) diff --git a/.stats.yml b/.stats.yml index 78eff165..9a94e549 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ -configured_endpoints: 18 +configured_endpoints: 19 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fbrowserbase-b92143ddb16135de4ff65ce8bcdfe9991d11c73570f42f07ea27e0da86209a44.yml openapi_spec_hash: 16eb6e6c9687f01d2a791775b27dc315 -config_hash: b3ca4ec5b02e5333af51ebc2e9fdef1b +config_hash: b01d72cbe03bd762a73b05744086b2ec diff --git a/api.md b/api.md index dbb776f6..d2d26e57 100644 --- a/api.md +++ b/api.md @@ -11,6 +11,7 @@ Methods: - client.contexts.create(\*\*params) -> ContextCreateResponse - client.contexts.retrieve(id) -> Context - client.contexts.update(id) -> ContextUpdateResponse +- client.contexts.delete(id) -> None # Extensions diff --git a/src/browserbase/resources/contexts.py b/src/browserbase/resources/contexts.py index 4379aa91..03af85f0 100644 --- a/src/browserbase/resources/contexts.py +++ b/src/browserbase/resources/contexts.py @@ -5,7 +5,7 @@ import httpx from ..types import context_create_params -from .._types import Body, Omit, Query, Headers, NotGiven, omit, not_given +from .._types import Body, Omit, Query, Headers, NoneType, NotGiven, omit, not_given from .._utils import maybe_transform, async_maybe_transform from .._compat import cached_property from .._resource import SyncAPIResource, AsyncAPIResource @@ -145,6 +145,40 @@ def update( cast_to=ContextUpdateResponse, ) + def delete( + self, + 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, + ) -> None: + """ + Delete Context + + 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 id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + extra_headers = {"Accept": "*/*", **(extra_headers or {})} + return self._delete( + f"/v1/contexts/{id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=NoneType, + ) + class AsyncContextsResource(AsyncAPIResource): @cached_property @@ -268,6 +302,40 @@ async def update( cast_to=ContextUpdateResponse, ) + async def delete( + self, + 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, + ) -> None: + """ + Delete Context + + 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 id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + extra_headers = {"Accept": "*/*", **(extra_headers or {})} + return await self._delete( + f"/v1/contexts/{id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=NoneType, + ) + class ContextsResourceWithRawResponse: def __init__(self, contexts: ContextsResource) -> None: @@ -282,6 +350,9 @@ def __init__(self, contexts: ContextsResource) -> None: self.update = to_raw_response_wrapper( contexts.update, ) + self.delete = to_raw_response_wrapper( + contexts.delete, + ) class AsyncContextsResourceWithRawResponse: @@ -297,6 +368,9 @@ def __init__(self, contexts: AsyncContextsResource) -> None: self.update = async_to_raw_response_wrapper( contexts.update, ) + self.delete = async_to_raw_response_wrapper( + contexts.delete, + ) class ContextsResourceWithStreamingResponse: @@ -312,6 +386,9 @@ def __init__(self, contexts: ContextsResource) -> None: self.update = to_streamed_response_wrapper( contexts.update, ) + self.delete = to_streamed_response_wrapper( + contexts.delete, + ) class AsyncContextsResourceWithStreamingResponse: @@ -327,3 +404,6 @@ def __init__(self, contexts: AsyncContextsResource) -> None: self.update = async_to_streamed_response_wrapper( contexts.update, ) + self.delete = async_to_streamed_response_wrapper( + contexts.delete, + ) diff --git a/tests/api_resources/test_contexts.py b/tests/api_resources/test_contexts.py index 83267ca7..31fb97d0 100644 --- a/tests/api_resources/test_contexts.py +++ b/tests/api_resources/test_contexts.py @@ -125,6 +125,44 @@ def test_path_params_update(self, client: Browserbase) -> None: "", ) + @parametrize + def test_method_delete(self, client: Browserbase) -> None: + context = client.contexts.delete( + "id", + ) + assert context is None + + @parametrize + def test_raw_response_delete(self, client: Browserbase) -> None: + response = client.contexts.with_raw_response.delete( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + context = response.parse() + assert context is None + + @parametrize + def test_streaming_response_delete(self, client: Browserbase) -> None: + with client.contexts.with_streaming_response.delete( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + context = response.parse() + assert context is None + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_delete(self, client: Browserbase) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.contexts.with_raw_response.delete( + "", + ) + class TestAsyncContexts: parametrize = pytest.mark.parametrize( @@ -238,3 +276,41 @@ async def test_path_params_update(self, async_client: AsyncBrowserbase) -> None: await async_client.contexts.with_raw_response.update( "", ) + + @parametrize + async def test_method_delete(self, async_client: AsyncBrowserbase) -> None: + context = await async_client.contexts.delete( + "id", + ) + assert context is None + + @parametrize + async def test_raw_response_delete(self, async_client: AsyncBrowserbase) -> None: + response = await async_client.contexts.with_raw_response.delete( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + context = await response.parse() + assert context is None + + @parametrize + async def test_streaming_response_delete(self, async_client: AsyncBrowserbase) -> None: + async with async_client.contexts.with_streaming_response.delete( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + context = await response.parse() + assert context is None + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_delete(self, async_client: AsyncBrowserbase) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.contexts.with_raw_response.delete( + "", + ) From 489eb030724b9e74dc0b2685e52ff3eb3f02d030 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 23 Feb 2026 22:51:03 +0000 Subject: [PATCH 241/330] feat(api): api update --- .stats.yml | 8 +- README.md | 1 + api.md | 29 ++-- src/browserbase/resources/contexts.py | 104 ++------------ src/browserbase/resources/extensions.py | 27 ++-- src/browserbase/resources/projects.py | 32 ++--- .../resources/sessions/sessions.py | 86 ++++++------ src/browserbase/types/__init__.py | 13 +- .../types/context_create_params.py | 4 +- ...ontext.py => context_retrieve_response.py} | 4 +- ...ension.py => extension_create_response.py} | 4 +- .../types/extension_retrieve_response.py | 22 +++ .../types/project_list_response.py | 27 +++- ...roject.py => project_retrieve_response.py} | 4 +- ...ect_usage.py => project_usage_response.py} | 4 +- .../types/session_create_params.py | 92 +++++++----- ...live_urls.py => session_debug_response.py} | 4 +- .../types/session_list_response.py | 58 +++++++- .../types/session_update_params.py | 12 +- ...{session.py => session_update_response.py} | 4 +- src/browserbase/types/sessions/__init__.py | 2 - .../types/sessions/log_list_response.py | 48 ++++++- .../sessions/recording_retrieve_response.py | 26 +++- src/browserbase/types/sessions/session_log.py | 46 ------ .../types/sessions/session_recording.py | 24 ---- tests/api_resources/test_contexts.py | 120 +++------------- tests/api_resources/test_extensions.py | 26 ++-- tests/api_resources/test_projects.py | 26 ++-- tests/api_resources/test_sessions.py | 132 ++++++++++++------ tests/test_client.py | 28 ++-- 30 files changed, 514 insertions(+), 503 deletions(-) rename src/browserbase/types/{context.py => context_retrieve_response.py} (84%) rename src/browserbase/types/{extension.py => extension_create_response.py} (85%) create mode 100644 src/browserbase/types/extension_retrieve_response.py rename src/browserbase/types/{project.py => project_retrieve_response.py} (87%) rename src/browserbase/types/{project_usage.py => project_usage_response.py} (78%) rename src/browserbase/types/{session_live_urls.py => session_debug_response.py} (88%) rename src/browserbase/types/{session.py => session_update_response.py} (95%) delete mode 100644 src/browserbase/types/sessions/session_log.py delete mode 100644 src/browserbase/types/sessions/session_recording.py diff --git a/.stats.yml b/.stats.yml index 9a94e549..b000c8c1 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ -configured_endpoints: 19 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fbrowserbase-b92143ddb16135de4ff65ce8bcdfe9991d11c73570f42f07ea27e0da86209a44.yml -openapi_spec_hash: 16eb6e6c9687f01d2a791775b27dc315 -config_hash: b01d72cbe03bd762a73b05744086b2ec +configured_endpoints: 18 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fbrowserbase-be7a4aeebb1605262935b4b3ab446a95b1fad8a7d18098943dd548c8a486ef13.yml +openapi_spec_hash: 1c950a109f80140711e7ae2cf87fddad +config_hash: b3ca4ec5b02e5333af51ebc2e9fdef1b diff --git a/README.md b/README.md index 5d384354..dcb4b768 100644 --- a/README.md +++ b/README.md @@ -122,6 +122,7 @@ from browserbase import Browserbase client = Browserbase() session = client.sessions.create( + project_id="projectId", browser_settings={}, ) print(session.browser_settings) diff --git a/api.md b/api.md index d2d26e57..01454851 100644 --- a/api.md +++ b/api.md @@ -3,28 +3,27 @@ Types: ```python -from browserbase.types import Context, ContextCreateResponse, ContextUpdateResponse +from browserbase.types import ContextCreateResponse, ContextRetrieveResponse, ContextUpdateResponse ``` Methods: - client.contexts.create(\*\*params) -> ContextCreateResponse -- client.contexts.retrieve(id) -> Context +- client.contexts.retrieve(id) -> ContextRetrieveResponse - client.contexts.update(id) -> ContextUpdateResponse -- client.contexts.delete(id) -> None # Extensions Types: ```python -from browserbase.types import Extension +from browserbase.types import ExtensionCreateResponse, ExtensionRetrieveResponse ``` Methods: -- client.extensions.create(\*\*params) -> Extension -- client.extensions.retrieve(id) -> Extension +- client.extensions.create(\*\*params) -> ExtensionCreateResponse +- client.extensions.retrieve(id) -> ExtensionRetrieveResponse - client.extensions.delete(id) -> None # Projects @@ -32,14 +31,14 @@ Methods: Types: ```python -from browserbase.types import Project, ProjectUsage, ProjectListResponse +from browserbase.types import ProjectRetrieveResponse, ProjectListResponse, ProjectUsageResponse ``` Methods: -- client.projects.retrieve(id) -> Project +- client.projects.retrieve(id) -> ProjectRetrieveResponse - client.projects.list() -> ProjectListResponse -- client.projects.usage(id) -> ProjectUsage +- client.projects.usage(id) -> ProjectUsageResponse # Sessions @@ -47,11 +46,11 @@ Types: ```python from browserbase.types import ( - Session, - SessionLiveURLs, SessionCreateResponse, SessionRetrieveResponse, + SessionUpdateResponse, SessionListResponse, + SessionDebugResponse, ) ``` @@ -59,9 +58,9 @@ Methods: - client.sessions.create(\*\*params) -> SessionCreateResponse - client.sessions.retrieve(id) -> SessionRetrieveResponse -- client.sessions.update(id, \*\*params) -> Session +- client.sessions.update(id, \*\*params) -> SessionUpdateResponse - client.sessions.list(\*\*params) -> SessionListResponse -- client.sessions.debug(id) -> SessionLiveURLs +- client.sessions.debug(id) -> SessionDebugResponse ## Downloads @@ -74,7 +73,7 @@ Methods: Types: ```python -from browserbase.types.sessions import SessionLog, LogListResponse +from browserbase.types.sessions import LogListResponse ``` Methods: @@ -86,7 +85,7 @@ Methods: Types: ```python -from browserbase.types.sessions import SessionRecording, RecordingRetrieveResponse +from browserbase.types.sessions import RecordingRetrieveResponse ``` Methods: diff --git a/src/browserbase/resources/contexts.py b/src/browserbase/resources/contexts.py index 03af85f0..d2bb4167 100644 --- a/src/browserbase/resources/contexts.py +++ b/src/browserbase/resources/contexts.py @@ -5,7 +5,7 @@ import httpx from ..types import context_create_params -from .._types import Body, Omit, Query, Headers, NoneType, NotGiven, omit, not_given +from .._types import Body, Query, Headers, NotGiven, not_given from .._utils import maybe_transform, async_maybe_transform from .._compat import cached_property from .._resource import SyncAPIResource, AsyncAPIResource @@ -16,9 +16,9 @@ async_to_streamed_response_wrapper, ) from .._base_client import make_request_options -from ..types.context import Context from ..types.context_create_response import ContextCreateResponse from ..types.context_update_response import ContextUpdateResponse +from ..types.context_retrieve_response import ContextRetrieveResponse __all__ = ["ContextsResource", "AsyncContextsResource"] @@ -46,7 +46,7 @@ def with_streaming_response(self) -> ContextsResourceWithStreamingResponse: def create( self, *, - project_id: str | Omit = omit, + project_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, @@ -89,9 +89,9 @@ def retrieve( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> Context: + ) -> ContextRetrieveResponse: """ - Context + Get a Context Args: extra_headers: Send extra headers @@ -109,7 +109,7 @@ def retrieve( options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=Context, + cast_to=ContextRetrieveResponse, ) def update( @@ -124,7 +124,7 @@ def update( timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> ContextUpdateResponse: """ - Update Context + Update a Context Args: extra_headers: Send extra headers @@ -145,40 +145,6 @@ def update( cast_to=ContextUpdateResponse, ) - def delete( - self, - 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, - ) -> None: - """ - Delete Context - - 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 id: - raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") - extra_headers = {"Accept": "*/*", **(extra_headers or {})} - return self._delete( - f"/v1/contexts/{id}", - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=NoneType, - ) - class AsyncContextsResource(AsyncAPIResource): @cached_property @@ -203,7 +169,7 @@ def with_streaming_response(self) -> AsyncContextsResourceWithStreamingResponse: async def create( self, *, - project_id: str | Omit = omit, + project_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, @@ -246,9 +212,9 @@ async def retrieve( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> Context: + ) -> ContextRetrieveResponse: """ - Context + Get a Context Args: extra_headers: Send extra headers @@ -266,7 +232,7 @@ async def retrieve( options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=Context, + cast_to=ContextRetrieveResponse, ) async def update( @@ -281,7 +247,7 @@ async def update( timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> ContextUpdateResponse: """ - Update Context + Update a Context Args: extra_headers: Send extra headers @@ -302,40 +268,6 @@ async def update( cast_to=ContextUpdateResponse, ) - async def delete( - self, - 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, - ) -> None: - """ - Delete Context - - 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 id: - raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") - extra_headers = {"Accept": "*/*", **(extra_headers or {})} - return await self._delete( - f"/v1/contexts/{id}", - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=NoneType, - ) - class ContextsResourceWithRawResponse: def __init__(self, contexts: ContextsResource) -> None: @@ -350,9 +282,6 @@ def __init__(self, contexts: ContextsResource) -> None: self.update = to_raw_response_wrapper( contexts.update, ) - self.delete = to_raw_response_wrapper( - contexts.delete, - ) class AsyncContextsResourceWithRawResponse: @@ -368,9 +297,6 @@ def __init__(self, contexts: AsyncContextsResource) -> None: self.update = async_to_raw_response_wrapper( contexts.update, ) - self.delete = async_to_raw_response_wrapper( - contexts.delete, - ) class ContextsResourceWithStreamingResponse: @@ -386,9 +312,6 @@ def __init__(self, contexts: ContextsResource) -> None: self.update = to_streamed_response_wrapper( contexts.update, ) - self.delete = to_streamed_response_wrapper( - contexts.delete, - ) class AsyncContextsResourceWithStreamingResponse: @@ -404,6 +327,3 @@ def __init__(self, contexts: AsyncContextsResource) -> None: self.update = async_to_streamed_response_wrapper( contexts.update, ) - self.delete = async_to_streamed_response_wrapper( - contexts.delete, - ) diff --git a/src/browserbase/resources/extensions.py b/src/browserbase/resources/extensions.py index 882a495f..21d06e70 100644 --- a/src/browserbase/resources/extensions.py +++ b/src/browserbase/resources/extensions.py @@ -18,7 +18,8 @@ async_to_streamed_response_wrapper, ) from .._base_client import make_request_options -from ..types.extension import Extension +from ..types.extension_create_response import ExtensionCreateResponse +from ..types.extension_retrieve_response import ExtensionRetrieveResponse __all__ = ["ExtensionsResource", "AsyncExtensionsResource"] @@ -53,7 +54,7 @@ def create( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> Extension: + ) -> ExtensionCreateResponse: """ Upload an Extension @@ -79,7 +80,7 @@ def create( options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=Extension, + cast_to=ExtensionCreateResponse, ) def retrieve( @@ -92,9 +93,9 @@ def retrieve( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> Extension: + ) -> ExtensionRetrieveResponse: """ - Extension + Get an Extension Args: extra_headers: Send extra headers @@ -112,7 +113,7 @@ def retrieve( options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=Extension, + cast_to=ExtensionRetrieveResponse, ) def delete( @@ -127,7 +128,7 @@ def delete( timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> None: """ - Delete Extension + Delete an Extension Args: extra_headers: Send extra headers @@ -180,7 +181,7 @@ async def create( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> Extension: + ) -> ExtensionCreateResponse: """ Upload an Extension @@ -206,7 +207,7 @@ async def create( options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=Extension, + cast_to=ExtensionCreateResponse, ) async def retrieve( @@ -219,9 +220,9 @@ async def retrieve( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> Extension: + ) -> ExtensionRetrieveResponse: """ - Extension + Get an Extension Args: extra_headers: Send extra headers @@ -239,7 +240,7 @@ async def retrieve( options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=Extension, + cast_to=ExtensionRetrieveResponse, ) async def delete( @@ -254,7 +255,7 @@ async def delete( timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> None: """ - Delete Extension + Delete an Extension Args: extra_headers: Send extra headers diff --git a/src/browserbase/resources/projects.py b/src/browserbase/resources/projects.py index a6ae6338..62c28afa 100644 --- a/src/browserbase/resources/projects.py +++ b/src/browserbase/resources/projects.py @@ -14,9 +14,9 @@ async_to_streamed_response_wrapper, ) from .._base_client import make_request_options -from ..types.project import Project -from ..types.project_usage import ProjectUsage from ..types.project_list_response import ProjectListResponse +from ..types.project_usage_response import ProjectUsageResponse +from ..types.project_retrieve_response import ProjectRetrieveResponse __all__ = ["ProjectsResource", "AsyncProjectsResource"] @@ -51,9 +51,9 @@ def retrieve( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> Project: + ) -> ProjectRetrieveResponse: """ - Project + Get a Project Args: extra_headers: Send extra headers @@ -71,7 +71,7 @@ def retrieve( options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=Project, + cast_to=ProjectRetrieveResponse, ) def list( @@ -84,7 +84,7 @@ def list( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> ProjectListResponse: - """List projects""" + """List Projects""" return self._get( "/v1/projects", options=make_request_options( @@ -103,9 +103,9 @@ def usage( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> ProjectUsage: + ) -> ProjectUsageResponse: """ - Project Usage + Get Project Usage Args: extra_headers: Send extra headers @@ -123,7 +123,7 @@ def usage( options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=ProjectUsage, + cast_to=ProjectUsageResponse, ) @@ -157,9 +157,9 @@ async def retrieve( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> Project: + ) -> ProjectRetrieveResponse: """ - Project + Get a Project Args: extra_headers: Send extra headers @@ -177,7 +177,7 @@ async def retrieve( options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=Project, + cast_to=ProjectRetrieveResponse, ) async def list( @@ -190,7 +190,7 @@ async def list( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> ProjectListResponse: - """List projects""" + """List Projects""" return await self._get( "/v1/projects", options=make_request_options( @@ -209,9 +209,9 @@ async def usage( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> ProjectUsage: + ) -> ProjectUsageResponse: """ - Project Usage + Get Project Usage Args: extra_headers: Send extra headers @@ -229,7 +229,7 @@ async def usage( options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=ProjectUsage, + cast_to=ProjectUsageResponse, ) diff --git a/src/browserbase/resources/sessions/sessions.py b/src/browserbase/resources/sessions/sessions.py index 09fd15a3..ceaaeb81 100644 --- a/src/browserbase/resources/sessions/sessions.py +++ b/src/browserbase/resources/sessions/sessions.py @@ -51,10 +51,10 @@ async_to_streamed_response_wrapper, ) from ..._base_client import make_request_options -from ...types.session import Session -from ...types.session_live_urls import SessionLiveURLs from ...types.session_list_response import SessionListResponse +from ...types.session_debug_response import SessionDebugResponse from ...types.session_create_response import SessionCreateResponse +from ...types.session_update_response import SessionUpdateResponse from ...types.session_retrieve_response import SessionRetrieveResponse __all__ = ["SessionsResource", "AsyncSessionsResource"] @@ -99,11 +99,11 @@ def with_streaming_response(self) -> SessionsResourceWithStreamingResponse: def create( self, *, + project_id: str, browser_settings: session_create_params.BrowserSettings | Omit = omit, extension_id: str | Omit = omit, keep_alive: bool | Omit = omit, - project_id: str | Omit = omit, - proxies: Union[bool, Iterable[session_create_params.ProxiesUnionMember1]] | Omit = omit, + proxies: Union[Iterable[session_create_params.ProxiesUnionMember0], bool] | Omit = omit, region: Literal["us-west-2", "us-east-1", "eu-central-1", "ap-southeast-1"] | Omit = omit, api_timeout: int | Omit = omit, user_metadata: Dict[str, object] | Omit = omit, @@ -117,17 +117,17 @@ def create( """Create a Session Args: - extension_id: The uploaded Extension ID. + project_id: The Project ID. - See + Can be found in + [Settings](https://www.browserbase.com/settings). + + extension_id: The uploaded Extension ID. See [Upload Extension](/reference/api/upload-an-extension). keep_alive: Set to true to keep the session alive even after disconnections. Available on the Hobby Plan and above. - project_id: The Project ID. Can be found in - [Settings](https://www.browserbase.com/settings). - proxies: Proxy configuration. Can be true for default proxy, or an array of proxy configurations. @@ -151,10 +151,10 @@ def create( "/v1/sessions", body=maybe_transform( { + "project_id": project_id, "browser_settings": browser_settings, "extension_id": extension_id, "keep_alive": keep_alive, - "project_id": project_id, "proxies": proxies, "region": region, "api_timeout": api_timeout, @@ -180,7 +180,7 @@ def retrieve( timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> SessionRetrieveResponse: """ - Session + Get a Session Args: extra_headers: Send extra headers @@ -205,25 +205,26 @@ def update( self, id: str, *, + project_id: str, status: Literal["REQUEST_RELEASE"], - project_id: str | 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, - ) -> Session: - """ - Update Session + ) -> SessionUpdateResponse: + """Update a Session Args: - status: Set to `REQUEST_RELEASE` to request that the session complete. Use before - session's timeout to avoid additional charges. + project_id: The Project ID. - project_id: The Project ID. Can be found in + Can be found in [Settings](https://www.browserbase.com/settings). + status: Set to `REQUEST_RELEASE` to request that the session complete. Use before + session's timeout to avoid additional charges. + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -238,15 +239,15 @@ def update( f"/v1/sessions/{id}", body=maybe_transform( { - "status": status, "project_id": project_id, + "status": status, }, session_update_params.SessionUpdateParams, ), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=Session, + cast_to=SessionUpdateResponse, ) def list( @@ -306,7 +307,7 @@ def debug( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> SessionLiveURLs: + ) -> SessionDebugResponse: """ Session Live URLs @@ -326,7 +327,7 @@ def debug( options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=SessionLiveURLs, + cast_to=SessionDebugResponse, ) @@ -369,11 +370,11 @@ def with_streaming_response(self) -> AsyncSessionsResourceWithStreamingResponse: async def create( self, *, + project_id: str, browser_settings: session_create_params.BrowserSettings | Omit = omit, extension_id: str | Omit = omit, keep_alive: bool | Omit = omit, - project_id: str | Omit = omit, - proxies: Union[bool, Iterable[session_create_params.ProxiesUnionMember1]] | Omit = omit, + proxies: Union[Iterable[session_create_params.ProxiesUnionMember0], bool] | Omit = omit, region: Literal["us-west-2", "us-east-1", "eu-central-1", "ap-southeast-1"] | Omit = omit, api_timeout: int | Omit = omit, user_metadata: Dict[str, object] | Omit = omit, @@ -387,17 +388,17 @@ async def create( """Create a Session Args: - extension_id: The uploaded Extension ID. + project_id: The Project ID. - See + Can be found in + [Settings](https://www.browserbase.com/settings). + + extension_id: The uploaded Extension ID. See [Upload Extension](/reference/api/upload-an-extension). keep_alive: Set to true to keep the session alive even after disconnections. Available on the Hobby Plan and above. - project_id: The Project ID. Can be found in - [Settings](https://www.browserbase.com/settings). - proxies: Proxy configuration. Can be true for default proxy, or an array of proxy configurations. @@ -421,10 +422,10 @@ async def create( "/v1/sessions", body=await async_maybe_transform( { + "project_id": project_id, "browser_settings": browser_settings, "extension_id": extension_id, "keep_alive": keep_alive, - "project_id": project_id, "proxies": proxies, "region": region, "api_timeout": api_timeout, @@ -450,7 +451,7 @@ async def retrieve( timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> SessionRetrieveResponse: """ - Session + Get a Session Args: extra_headers: Send extra headers @@ -475,25 +476,26 @@ async def update( self, id: str, *, + project_id: str, status: Literal["REQUEST_RELEASE"], - project_id: str | 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, - ) -> Session: - """ - Update Session + ) -> SessionUpdateResponse: + """Update a Session Args: - status: Set to `REQUEST_RELEASE` to request that the session complete. Use before - session's timeout to avoid additional charges. + project_id: The Project ID. - project_id: The Project ID. Can be found in + Can be found in [Settings](https://www.browserbase.com/settings). + status: Set to `REQUEST_RELEASE` to request that the session complete. Use before + session's timeout to avoid additional charges. + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -508,15 +510,15 @@ async def update( f"/v1/sessions/{id}", body=await async_maybe_transform( { - "status": status, "project_id": project_id, + "status": status, }, session_update_params.SessionUpdateParams, ), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=Session, + cast_to=SessionUpdateResponse, ) async def list( @@ -576,7 +578,7 @@ async def debug( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> SessionLiveURLs: + ) -> SessionDebugResponse: """ Session Live URLs @@ -596,7 +598,7 @@ async def debug( options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=SessionLiveURLs, + cast_to=SessionDebugResponse, ) diff --git a/src/browserbase/types/__init__.py b/src/browserbase/types/__init__.py index 4dd85ddb..20e2f905 100644 --- a/src/browserbase/types/__init__.py +++ b/src/browserbase/types/__init__.py @@ -2,20 +2,21 @@ from __future__ import annotations -from .context import Context as Context -from .project import Project as Project -from .session import Session as Session -from .extension import Extension as Extension -from .project_usage import ProjectUsage as ProjectUsage -from .session_live_urls import SessionLiveURLs as SessionLiveURLs from .session_list_params import SessionListParams as SessionListParams from .context_create_params import ContextCreateParams as ContextCreateParams from .project_list_response import ProjectListResponse as ProjectListResponse from .session_create_params import SessionCreateParams as SessionCreateParams from .session_list_response import SessionListResponse as SessionListResponse from .session_update_params import SessionUpdateParams as SessionUpdateParams +from .project_usage_response import ProjectUsageResponse as ProjectUsageResponse +from .session_debug_response import SessionDebugResponse as SessionDebugResponse from .context_create_response import ContextCreateResponse as ContextCreateResponse from .context_update_response import ContextUpdateResponse as ContextUpdateResponse from .extension_create_params import ExtensionCreateParams as ExtensionCreateParams from .session_create_response import SessionCreateResponse as SessionCreateResponse +from .session_update_response import SessionUpdateResponse as SessionUpdateResponse +from .context_retrieve_response import ContextRetrieveResponse as ContextRetrieveResponse +from .extension_create_response import ExtensionCreateResponse as ExtensionCreateResponse +from .project_retrieve_response import ProjectRetrieveResponse as ProjectRetrieveResponse from .session_retrieve_response import SessionRetrieveResponse as SessionRetrieveResponse +from .extension_retrieve_response import ExtensionRetrieveResponse as ExtensionRetrieveResponse diff --git a/src/browserbase/types/context_create_params.py b/src/browserbase/types/context_create_params.py index 66c6c468..75cd1fcd 100644 --- a/src/browserbase/types/context_create_params.py +++ b/src/browserbase/types/context_create_params.py @@ -2,7 +2,7 @@ from __future__ import annotations -from typing_extensions import Annotated, TypedDict +from typing_extensions import Required, Annotated, TypedDict from .._utils import PropertyInfo @@ -10,7 +10,7 @@ class ContextCreateParams(TypedDict, total=False): - project_id: Annotated[str, PropertyInfo(alias="projectId")] + project_id: Required[Annotated[str, PropertyInfo(alias="projectId")]] """The Project ID. Can be found in [Settings](https://www.browserbase.com/settings). diff --git a/src/browserbase/types/context.py b/src/browserbase/types/context_retrieve_response.py similarity index 84% rename from src/browserbase/types/context.py rename to src/browserbase/types/context_retrieve_response.py index cb5c32fd..c2cd6925 100644 --- a/src/browserbase/types/context.py +++ b/src/browserbase/types/context_retrieve_response.py @@ -6,10 +6,10 @@ from .._models import BaseModel -__all__ = ["Context"] +__all__ = ["ContextRetrieveResponse"] -class Context(BaseModel): +class ContextRetrieveResponse(BaseModel): id: str created_at: datetime = FieldInfo(alias="createdAt") diff --git a/src/browserbase/types/extension.py b/src/browserbase/types/extension_create_response.py similarity index 85% rename from src/browserbase/types/extension.py rename to src/browserbase/types/extension_create_response.py index 94582c34..d2b74f41 100644 --- a/src/browserbase/types/extension.py +++ b/src/browserbase/types/extension_create_response.py @@ -6,10 +6,10 @@ from .._models import BaseModel -__all__ = ["Extension"] +__all__ = ["ExtensionCreateResponse"] -class Extension(BaseModel): +class ExtensionCreateResponse(BaseModel): id: str created_at: datetime = FieldInfo(alias="createdAt") diff --git a/src/browserbase/types/extension_retrieve_response.py b/src/browserbase/types/extension_retrieve_response.py new file mode 100644 index 00000000..c786348e --- /dev/null +++ b/src/browserbase/types/extension_retrieve_response.py @@ -0,0 +1,22 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from datetime import datetime + +from pydantic import Field as FieldInfo + +from .._models import BaseModel + +__all__ = ["ExtensionRetrieveResponse"] + + +class ExtensionRetrieveResponse(BaseModel): + id: str + + created_at: datetime = FieldInfo(alias="createdAt") + + file_name: str = FieldInfo(alias="fileName") + + project_id: str = FieldInfo(alias="projectId") + """The Project ID linked to the uploaded Extension.""" + + updated_at: datetime = FieldInfo(alias="updatedAt") diff --git a/src/browserbase/types/project_list_response.py b/src/browserbase/types/project_list_response.py index 2d05a236..e364b520 100644 --- a/src/browserbase/types/project_list_response.py +++ b/src/browserbase/types/project_list_response.py @@ -1,10 +1,31 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. from typing import List +from datetime import datetime from typing_extensions import TypeAlias -from .project import Project +from pydantic import Field as FieldInfo -__all__ = ["ProjectListResponse"] +from .._models import BaseModel -ProjectListResponse: TypeAlias = List[Project] +__all__ = ["ProjectListResponse", "ProjectListResponseItem"] + + +class ProjectListResponseItem(BaseModel): + id: str + + concurrency: int + """The maximum number of sessions that this project can run concurrently.""" + + created_at: datetime = FieldInfo(alias="createdAt") + + default_timeout: int = FieldInfo(alias="defaultTimeout") + + name: str + + owner_id: str = FieldInfo(alias="ownerId") + + updated_at: datetime = FieldInfo(alias="updatedAt") + + +ProjectListResponse: TypeAlias = List[ProjectListResponseItem] diff --git a/src/browserbase/types/project.py b/src/browserbase/types/project_retrieve_response.py similarity index 87% rename from src/browserbase/types/project.py rename to src/browserbase/types/project_retrieve_response.py index dc3cf335..78126679 100644 --- a/src/browserbase/types/project.py +++ b/src/browserbase/types/project_retrieve_response.py @@ -6,10 +6,10 @@ from .._models import BaseModel -__all__ = ["Project"] +__all__ = ["ProjectRetrieveResponse"] -class Project(BaseModel): +class ProjectRetrieveResponse(BaseModel): id: str concurrency: int diff --git a/src/browserbase/types/project_usage.py b/src/browserbase/types/project_usage_response.py similarity index 78% rename from src/browserbase/types/project_usage.py rename to src/browserbase/types/project_usage_response.py index c8a03f5b..b52fccfe 100644 --- a/src/browserbase/types/project_usage.py +++ b/src/browserbase/types/project_usage_response.py @@ -4,10 +4,10 @@ from .._models import BaseModel -__all__ = ["ProjectUsage"] +__all__ = ["ProjectUsageResponse"] -class ProjectUsage(BaseModel): +class ProjectUsageResponse(BaseModel): browser_minutes: int = FieldInfo(alias="browserMinutes") proxy_bytes: int = FieldInfo(alias="proxyBytes") diff --git a/src/browserbase/types/session_create_params.py b/src/browserbase/types/session_create_params.py index 33dd6fd5..2ba36400 100644 --- a/src/browserbase/types/session_create_params.py +++ b/src/browserbase/types/session_create_params.py @@ -2,25 +2,33 @@ from __future__ import annotations -from typing import Dict, Union, Iterable +from typing import Dict, List, Union, Iterable from typing_extensions import Literal, Required, Annotated, TypeAlias, TypedDict +from .._types import SequenceNotStr from .._utils import PropertyInfo __all__ = [ "SessionCreateParams", "BrowserSettings", "BrowserSettingsContext", + "BrowserSettingsFingerprint", + "BrowserSettingsFingerprintScreen", "BrowserSettingsViewport", - "ProxiesUnionMember1", - "ProxiesUnionMember1BrowserbaseProxyConfig", - "ProxiesUnionMember1BrowserbaseProxyConfigGeolocation", - "ProxiesUnionMember1ExternalProxyConfig", - "ProxiesUnionMember1NoneProxyConfig", + "ProxiesUnionMember0", + "ProxiesUnionMember0UnionMember0", + "ProxiesUnionMember0UnionMember0Geolocation", + "ProxiesUnionMember0UnionMember1", ] class SessionCreateParams(TypedDict, total=False): + project_id: Required[Annotated[str, PropertyInfo(alias="projectId")]] + """The Project ID. + + Can be found in [Settings](https://www.browserbase.com/settings). + """ + browser_settings: Annotated[BrowserSettings, PropertyInfo(alias="browserSettings")] extension_id: Annotated[str, PropertyInfo(alias="extensionId")] @@ -35,13 +43,7 @@ class SessionCreateParams(TypedDict, total=False): Available on the Hobby Plan and above. """ - project_id: Annotated[str, PropertyInfo(alias="projectId")] - """The Project ID. - - Can be found in [Settings](https://www.browserbase.com/settings). - """ - - proxies: Union[bool, Iterable[ProxiesUnionMember1]] + proxies: Union[Iterable[ProxiesUnionMember0], bool] """Proxy configuration. Can be true for default proxy, or an array of proxy configurations. @@ -72,10 +74,42 @@ class BrowserSettingsContext(TypedDict, total=False): """Whether or not to persist the context after browsing. Defaults to `false`.""" +class BrowserSettingsFingerprintScreen(TypedDict, total=False): + max_height: Annotated[int, PropertyInfo(alias="maxHeight")] + + max_width: Annotated[int, PropertyInfo(alias="maxWidth")] + + min_height: Annotated[int, PropertyInfo(alias="minHeight")] + + min_width: Annotated[int, PropertyInfo(alias="minWidth")] + + +class BrowserSettingsFingerprint(TypedDict, total=False): + """ + See usage examples [on the Stealth Mode page](/features/stealth-mode#fingerprinting) + """ + + 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: BrowserSettingsFingerprintScreen + + class BrowserSettingsViewport(TypedDict, total=False): height: int + """The height of the browser.""" width: int + """The width of the browser.""" class BrowserSettings(TypedDict, total=False): @@ -105,6 +139,12 @@ class BrowserSettings(TypedDict, total=False): See [Upload Extension](/reference/api/upload-an-extension). """ + fingerprint: BrowserSettingsFingerprint + """ + See usage examples + [on the Stealth Mode page](/features/stealth-mode#fingerprinting) + """ + log_session: Annotated[bool, PropertyInfo(alias="logSession")] """Enable or disable session logging. Defaults to `true`.""" @@ -123,8 +163,8 @@ class BrowserSettings(TypedDict, total=False): viewport: BrowserSettingsViewport -class ProxiesUnionMember1BrowserbaseProxyConfigGeolocation(TypedDict, total=False): - """Configuration for geolocation""" +class ProxiesUnionMember0UnionMember0Geolocation(TypedDict, total=False): + """Geographic location for the proxy. Optional.""" country: Required[str] """Country code in ISO 3166-1 alpha-2 format""" @@ -136,7 +176,7 @@ class ProxiesUnionMember1BrowserbaseProxyConfigGeolocation(TypedDict, total=Fals """US state code (2 characters). Must also specify US as the country. Optional.""" -class ProxiesUnionMember1BrowserbaseProxyConfig(TypedDict, total=False): +class ProxiesUnionMember0UnionMember0(TypedDict, total=False): type: Required[Literal["browserbase"]] """Type of proxy. @@ -149,11 +189,11 @@ class ProxiesUnionMember1BrowserbaseProxyConfig(TypedDict, total=False): If omitted, defaults to all domains. Optional. """ - geolocation: ProxiesUnionMember1BrowserbaseProxyConfigGeolocation - """Configuration for geolocation""" + geolocation: ProxiesUnionMember0UnionMember0Geolocation + """Geographic location for the proxy. Optional.""" -class ProxiesUnionMember1ExternalProxyConfig(TypedDict, total=False): +class ProxiesUnionMember0UnionMember1(TypedDict, total=False): server: Required[str] """Server URL for external proxy. Required.""" @@ -173,16 +213,4 @@ class ProxiesUnionMember1ExternalProxyConfig(TypedDict, total=False): """Username for external proxy authentication. Optional.""" -class ProxiesUnionMember1NoneProxyConfig(TypedDict, total=False): - domain_pattern: Required[Annotated[str, PropertyInfo(alias="domainPattern")]] - """Domain pattern for which site should have proxies disabled.""" - - type: Required[Literal["none"]] - """Type of proxy. Use 'none' to disable proxy for matching domains.""" - - -ProxiesUnionMember1: TypeAlias = Union[ - ProxiesUnionMember1BrowserbaseProxyConfig, - ProxiesUnionMember1ExternalProxyConfig, - ProxiesUnionMember1NoneProxyConfig, -] +ProxiesUnionMember0: TypeAlias = Union[ProxiesUnionMember0UnionMember0, ProxiesUnionMember0UnionMember1] diff --git a/src/browserbase/types/session_live_urls.py b/src/browserbase/types/session_debug_response.py similarity index 88% rename from src/browserbase/types/session_live_urls.py rename to src/browserbase/types/session_debug_response.py index 3c7ba320..9cee7a77 100644 --- a/src/browserbase/types/session_live_urls.py +++ b/src/browserbase/types/session_debug_response.py @@ -6,7 +6,7 @@ from .._models import BaseModel -__all__ = ["SessionLiveURLs", "Page"] +__all__ = ["SessionDebugResponse", "Page"] class Page(BaseModel): @@ -23,7 +23,7 @@ class Page(BaseModel): url: str -class SessionLiveURLs(BaseModel): +class SessionDebugResponse(BaseModel): debugger_fullscreen_url: str = FieldInfo(alias="debuggerFullscreenUrl") debugger_url: str = FieldInfo(alias="debuggerUrl") diff --git a/src/browserbase/types/session_list_response.py b/src/browserbase/types/session_list_response.py index ca162ddb..4c1bd885 100644 --- a/src/browserbase/types/session_list_response.py +++ b/src/browserbase/types/session_list_response.py @@ -1,10 +1,58 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -from typing import List -from typing_extensions import TypeAlias +from typing import Dict, List, Optional +from datetime import datetime +from typing_extensions import Literal, TypeAlias -from .session import Session +from pydantic import Field as FieldInfo -__all__ = ["SessionListResponse"] +from .._models import BaseModel -SessionListResponse: TypeAlias = List[Session] +__all__ = ["SessionListResponse", "SessionListResponseItem"] + + +class SessionListResponseItem(BaseModel): + id: str + + created_at: datetime = FieldInfo(alias="createdAt") + + expires_at: datetime = FieldInfo(alias="expiresAt") + + keep_alive: bool = FieldInfo(alias="keepAlive") + """Indicates if the Session was created to be kept alive upon disconnections""" + + project_id: str = FieldInfo(alias="projectId") + """The Project ID linked to the Session.""" + + proxy_bytes: int = FieldInfo(alias="proxyBytes") + """Bytes used via the [Proxy](/features/stealth-mode#proxies-and-residential-ips)""" + + region: Literal["us-west-2", "us-east-1", "eu-central-1", "ap-southeast-1"] + """The region where the Session is running.""" + + started_at: datetime = FieldInfo(alias="startedAt") + + status: Literal["RUNNING", "ERROR", "TIMED_OUT", "COMPLETED"] + + updated_at: datetime = FieldInfo(alias="updatedAt") + + avg_cpu_usage: Optional[int] = FieldInfo(alias="avgCpuUsage", default=None) + """CPU used by the Session""" + + context_id: Optional[str] = FieldInfo(alias="contextId", default=None) + """Optional. The Context linked to the Session.""" + + ended_at: Optional[datetime] = FieldInfo(alias="endedAt", default=None) + + memory_usage: Optional[int] = FieldInfo(alias="memoryUsage", default=None) + """Memory used by the Session""" + + user_metadata: Optional[Dict[str, object]] = FieldInfo(alias="userMetadata", default=None) + """Arbitrary user metadata to attach to the session. + + To learn more about user metadata, see + [User Metadata](/features/sessions#user-metadata). + """ + + +SessionListResponse: TypeAlias = List[SessionListResponseItem] diff --git a/src/browserbase/types/session_update_params.py b/src/browserbase/types/session_update_params.py index 71c589d9..66dcd351 100644 --- a/src/browserbase/types/session_update_params.py +++ b/src/browserbase/types/session_update_params.py @@ -10,14 +10,14 @@ class SessionUpdateParams(TypedDict, total=False): - status: Required[Literal["REQUEST_RELEASE"]] - """Set to `REQUEST_RELEASE` to request that the session complete. + project_id: Required[Annotated[str, PropertyInfo(alias="projectId")]] + """The Project ID. - Use before session's timeout to avoid additional charges. + Can be found in [Settings](https://www.browserbase.com/settings). """ - project_id: Annotated[str, PropertyInfo(alias="projectId")] - """The Project ID. + status: Required[Literal["REQUEST_RELEASE"]] + """Set to `REQUEST_RELEASE` to request that the session complete. - Can be found in [Settings](https://www.browserbase.com/settings). + Use before session's timeout to avoid additional charges. """ diff --git a/src/browserbase/types/session.py b/src/browserbase/types/session_update_response.py similarity index 95% rename from src/browserbase/types/session.py rename to src/browserbase/types/session_update_response.py index 16450e29..67a13711 100644 --- a/src/browserbase/types/session.py +++ b/src/browserbase/types/session_update_response.py @@ -8,10 +8,10 @@ from .._models import BaseModel -__all__ = ["Session"] +__all__ = ["SessionUpdateResponse"] -class Session(BaseModel): +class SessionUpdateResponse(BaseModel): id: str created_at: datetime = FieldInfo(alias="createdAt") diff --git a/src/browserbase/types/sessions/__init__.py b/src/browserbase/types/sessions/__init__.py index 0cef6b19..69d54703 100644 --- a/src/browserbase/types/sessions/__init__.py +++ b/src/browserbase/types/sessions/__init__.py @@ -2,9 +2,7 @@ from __future__ import annotations -from .session_log import SessionLog as SessionLog from .log_list_response import LogListResponse as LogListResponse -from .session_recording import SessionRecording as SessionRecording from .upload_create_params import UploadCreateParams as UploadCreateParams from .upload_create_response import UploadCreateResponse as UploadCreateResponse from .recording_retrieve_response import RecordingRetrieveResponse as RecordingRetrieveResponse diff --git a/src/browserbase/types/sessions/log_list_response.py b/src/browserbase/types/sessions/log_list_response.py index 2b325a8c..efd848ab 100644 --- a/src/browserbase/types/sessions/log_list_response.py +++ b/src/browserbase/types/sessions/log_list_response.py @@ -1,10 +1,50 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -from typing import List +from typing import Dict, List, Optional from typing_extensions import TypeAlias -from .session_log import SessionLog +from pydantic import Field as FieldInfo -__all__ = ["LogListResponse"] +from ..._models import BaseModel -LogListResponse: TypeAlias = List[SessionLog] +__all__ = ["LogListResponse", "LogListResponseItem", "LogListResponseItemRequest", "LogListResponseItemResponse"] + + +class LogListResponseItemRequest(BaseModel): + params: Dict[str, object] + + raw_body: str = FieldInfo(alias="rawBody") + + timestamp: Optional[int] = None + """milliseconds that have elapsed since the UNIX epoch""" + + +class LogListResponseItemResponse(BaseModel): + raw_body: str = FieldInfo(alias="rawBody") + + result: Dict[str, object] + + timestamp: Optional[int] = None + """milliseconds that have elapsed since the UNIX epoch""" + + +class LogListResponseItem(BaseModel): + method: str + + page_id: int = FieldInfo(alias="pageId") + + session_id: str = FieldInfo(alias="sessionId") + + frame_id: Optional[str] = FieldInfo(alias="frameId", default=None) + + loader_id: Optional[str] = FieldInfo(alias="loaderId", default=None) + + request: Optional[LogListResponseItemRequest] = None + + response: Optional[LogListResponseItemResponse] = None + + timestamp: Optional[int] = None + """milliseconds that have elapsed since the UNIX epoch""" + + +LogListResponse: TypeAlias = List[LogListResponseItem] diff --git a/src/browserbase/types/sessions/recording_retrieve_response.py b/src/browserbase/types/sessions/recording_retrieve_response.py index 951969bb..d3613b8c 100644 --- a/src/browserbase/types/sessions/recording_retrieve_response.py +++ b/src/browserbase/types/sessions/recording_retrieve_response.py @@ -1,10 +1,28 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -from typing import List +from typing import Dict, List from typing_extensions import TypeAlias -from .session_recording import SessionRecording +from pydantic import Field as FieldInfo -__all__ = ["RecordingRetrieveResponse"] +from ..._models import BaseModel -RecordingRetrieveResponse: TypeAlias = List[SessionRecording] +__all__ = ["RecordingRetrieveResponse", "RecordingRetrieveResponseItem"] + + +class RecordingRetrieveResponseItem(BaseModel): + data: Dict[str, object] + """ + See + [rrweb documentation](https://github.com/rrweb-io/rrweb/blob/master/docs/recipes/dive-into-event.md). + """ + + session_id: str = FieldInfo(alias="sessionId") + + timestamp: int + """milliseconds that have elapsed since the UNIX epoch""" + + type: int + + +RecordingRetrieveResponse: TypeAlias = List[RecordingRetrieveResponseItem] diff --git a/src/browserbase/types/sessions/session_log.py b/src/browserbase/types/sessions/session_log.py deleted file mode 100644 index 428f518a..00000000 --- a/src/browserbase/types/sessions/session_log.py +++ /dev/null @@ -1,46 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from typing import Dict, Optional - -from pydantic import Field as FieldInfo - -from ..._models import BaseModel - -__all__ = ["SessionLog", "Request", "Response"] - - -class Request(BaseModel): - params: Dict[str, object] - - raw_body: str = FieldInfo(alias="rawBody") - - timestamp: Optional[int] = None - """milliseconds that have elapsed since the UNIX epoch""" - - -class Response(BaseModel): - raw_body: str = FieldInfo(alias="rawBody") - - result: Dict[str, object] - - timestamp: Optional[int] = None - """milliseconds that have elapsed since the UNIX epoch""" - - -class SessionLog(BaseModel): - method: str - - page_id: int = FieldInfo(alias="pageId") - - session_id: str = FieldInfo(alias="sessionId") - - frame_id: Optional[str] = FieldInfo(alias="frameId", default=None) - - loader_id: Optional[str] = FieldInfo(alias="loaderId", default=None) - - request: Optional[Request] = None - - response: Optional[Response] = None - - timestamp: Optional[int] = None - """milliseconds that have elapsed since the UNIX epoch""" diff --git a/src/browserbase/types/sessions/session_recording.py b/src/browserbase/types/sessions/session_recording.py deleted file mode 100644 index c8471371..00000000 --- a/src/browserbase/types/sessions/session_recording.py +++ /dev/null @@ -1,24 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from typing import Dict - -from pydantic import Field as FieldInfo - -from ..._models import BaseModel - -__all__ = ["SessionRecording"] - - -class SessionRecording(BaseModel): - data: Dict[str, object] - """ - See - [rrweb documentation](https://github.com/rrweb-io/rrweb/blob/master/docs/recipes/dive-into-event.md). - """ - - session_id: str = FieldInfo(alias="sessionId") - - timestamp: int - """milliseconds that have elapsed since the UNIX epoch""" - - type: int diff --git a/tests/api_resources/test_contexts.py b/tests/api_resources/test_contexts.py index 31fb97d0..4ad27733 100644 --- a/tests/api_resources/test_contexts.py +++ b/tests/api_resources/test_contexts.py @@ -9,7 +9,11 @@ from browserbase import Browserbase, AsyncBrowserbase from tests.utils import assert_matches_type -from browserbase.types import Context, ContextCreateResponse, ContextUpdateResponse +from browserbase.types import ( + ContextCreateResponse, + ContextUpdateResponse, + ContextRetrieveResponse, +) base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") @@ -19,11 +23,6 @@ class TestContexts: @parametrize def test_method_create(self, client: Browserbase) -> None: - context = client.contexts.create() - assert_matches_type(ContextCreateResponse, context, path=["response"]) - - @parametrize - def test_method_create_with_all_params(self, client: Browserbase) -> None: context = client.contexts.create( project_id="projectId", ) @@ -31,7 +30,9 @@ def test_method_create_with_all_params(self, client: Browserbase) -> None: @parametrize def test_raw_response_create(self, client: Browserbase) -> None: - response = client.contexts.with_raw_response.create() + response = client.contexts.with_raw_response.create( + project_id="projectId", + ) assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -40,7 +41,9 @@ def test_raw_response_create(self, client: Browserbase) -> None: @parametrize def test_streaming_response_create(self, client: Browserbase) -> None: - with client.contexts.with_streaming_response.create() as response: + with client.contexts.with_streaming_response.create( + project_id="projectId", + ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -54,7 +57,7 @@ def test_method_retrieve(self, client: Browserbase) -> None: context = client.contexts.retrieve( "id", ) - assert_matches_type(Context, context, path=["response"]) + assert_matches_type(ContextRetrieveResponse, context, path=["response"]) @parametrize def test_raw_response_retrieve(self, client: Browserbase) -> None: @@ -65,7 +68,7 @@ def test_raw_response_retrieve(self, client: Browserbase) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" context = response.parse() - assert_matches_type(Context, context, path=["response"]) + assert_matches_type(ContextRetrieveResponse, context, path=["response"]) @parametrize def test_streaming_response_retrieve(self, client: Browserbase) -> None: @@ -76,7 +79,7 @@ def test_streaming_response_retrieve(self, client: Browserbase) -> None: assert response.http_request.headers.get("X-Stainless-Lang") == "python" context = response.parse() - assert_matches_type(Context, context, path=["response"]) + assert_matches_type(ContextRetrieveResponse, context, path=["response"]) assert cast(Any, response.is_closed) is True @@ -125,44 +128,6 @@ def test_path_params_update(self, client: Browserbase) -> None: "", ) - @parametrize - def test_method_delete(self, client: Browserbase) -> None: - context = client.contexts.delete( - "id", - ) - assert context is None - - @parametrize - def test_raw_response_delete(self, client: Browserbase) -> None: - response = client.contexts.with_raw_response.delete( - "id", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - context = response.parse() - assert context is None - - @parametrize - def test_streaming_response_delete(self, client: Browserbase) -> None: - with client.contexts.with_streaming_response.delete( - "id", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - context = response.parse() - assert context is None - - assert cast(Any, response.is_closed) is True - - @parametrize - def test_path_params_delete(self, client: Browserbase) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): - client.contexts.with_raw_response.delete( - "", - ) - class TestAsyncContexts: parametrize = pytest.mark.parametrize( @@ -171,11 +136,6 @@ class TestAsyncContexts: @parametrize async def test_method_create(self, async_client: AsyncBrowserbase) -> None: - context = await async_client.contexts.create() - assert_matches_type(ContextCreateResponse, context, path=["response"]) - - @parametrize - async def test_method_create_with_all_params(self, async_client: AsyncBrowserbase) -> None: context = await async_client.contexts.create( project_id="projectId", ) @@ -183,7 +143,9 @@ async def test_method_create_with_all_params(self, async_client: AsyncBrowserbas @parametrize async def test_raw_response_create(self, async_client: AsyncBrowserbase) -> None: - response = await async_client.contexts.with_raw_response.create() + response = await async_client.contexts.with_raw_response.create( + project_id="projectId", + ) assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -192,7 +154,9 @@ async def test_raw_response_create(self, async_client: AsyncBrowserbase) -> None @parametrize async def test_streaming_response_create(self, async_client: AsyncBrowserbase) -> None: - async with async_client.contexts.with_streaming_response.create() as response: + async with async_client.contexts.with_streaming_response.create( + project_id="projectId", + ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -206,7 +170,7 @@ async def test_method_retrieve(self, async_client: AsyncBrowserbase) -> None: context = await async_client.contexts.retrieve( "id", ) - assert_matches_type(Context, context, path=["response"]) + assert_matches_type(ContextRetrieveResponse, context, path=["response"]) @parametrize async def test_raw_response_retrieve(self, async_client: AsyncBrowserbase) -> None: @@ -217,7 +181,7 @@ async def test_raw_response_retrieve(self, async_client: AsyncBrowserbase) -> No assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" context = await response.parse() - assert_matches_type(Context, context, path=["response"]) + assert_matches_type(ContextRetrieveResponse, context, path=["response"]) @parametrize async def test_streaming_response_retrieve(self, async_client: AsyncBrowserbase) -> None: @@ -228,7 +192,7 @@ async def test_streaming_response_retrieve(self, async_client: AsyncBrowserbase) assert response.http_request.headers.get("X-Stainless-Lang") == "python" context = await response.parse() - assert_matches_type(Context, context, path=["response"]) + assert_matches_type(ContextRetrieveResponse, context, path=["response"]) assert cast(Any, response.is_closed) is True @@ -276,41 +240,3 @@ async def test_path_params_update(self, async_client: AsyncBrowserbase) -> None: await async_client.contexts.with_raw_response.update( "", ) - - @parametrize - async def test_method_delete(self, async_client: AsyncBrowserbase) -> None: - context = await async_client.contexts.delete( - "id", - ) - assert context is None - - @parametrize - async def test_raw_response_delete(self, async_client: AsyncBrowserbase) -> None: - response = await async_client.contexts.with_raw_response.delete( - "id", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - context = await response.parse() - assert context is None - - @parametrize - async def test_streaming_response_delete(self, async_client: AsyncBrowserbase) -> None: - async with async_client.contexts.with_streaming_response.delete( - "id", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - context = await response.parse() - assert context is None - - assert cast(Any, response.is_closed) is True - - @parametrize - async def test_path_params_delete(self, async_client: AsyncBrowserbase) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): - await async_client.contexts.with_raw_response.delete( - "", - ) diff --git a/tests/api_resources/test_extensions.py b/tests/api_resources/test_extensions.py index 6b6a0183..e32ae9b0 100644 --- a/tests/api_resources/test_extensions.py +++ b/tests/api_resources/test_extensions.py @@ -9,7 +9,7 @@ from browserbase import Browserbase, AsyncBrowserbase from tests.utils import assert_matches_type -from browserbase.types import Extension +from browserbase.types import ExtensionCreateResponse, ExtensionRetrieveResponse base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") @@ -22,7 +22,7 @@ def test_method_create(self, client: Browserbase) -> None: extension = client.extensions.create( file=b"raw file contents", ) - assert_matches_type(Extension, extension, path=["response"]) + assert_matches_type(ExtensionCreateResponse, extension, path=["response"]) @parametrize def test_raw_response_create(self, client: Browserbase) -> None: @@ -33,7 +33,7 @@ def test_raw_response_create(self, client: Browserbase) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" extension = response.parse() - assert_matches_type(Extension, extension, path=["response"]) + assert_matches_type(ExtensionCreateResponse, extension, path=["response"]) @parametrize def test_streaming_response_create(self, client: Browserbase) -> None: @@ -44,7 +44,7 @@ def test_streaming_response_create(self, client: Browserbase) -> None: assert response.http_request.headers.get("X-Stainless-Lang") == "python" extension = response.parse() - assert_matches_type(Extension, extension, path=["response"]) + assert_matches_type(ExtensionCreateResponse, extension, path=["response"]) assert cast(Any, response.is_closed) is True @@ -53,7 +53,7 @@ def test_method_retrieve(self, client: Browserbase) -> None: extension = client.extensions.retrieve( "id", ) - assert_matches_type(Extension, extension, path=["response"]) + assert_matches_type(ExtensionRetrieveResponse, extension, path=["response"]) @parametrize def test_raw_response_retrieve(self, client: Browserbase) -> None: @@ -64,7 +64,7 @@ def test_raw_response_retrieve(self, client: Browserbase) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" extension = response.parse() - assert_matches_type(Extension, extension, path=["response"]) + assert_matches_type(ExtensionRetrieveResponse, extension, path=["response"]) @parametrize def test_streaming_response_retrieve(self, client: Browserbase) -> None: @@ -75,7 +75,7 @@ def test_streaming_response_retrieve(self, client: Browserbase) -> None: assert response.http_request.headers.get("X-Stainless-Lang") == "python" extension = response.parse() - assert_matches_type(Extension, extension, path=["response"]) + assert_matches_type(ExtensionRetrieveResponse, extension, path=["response"]) assert cast(Any, response.is_closed) is True @@ -135,7 +135,7 @@ async def test_method_create(self, async_client: AsyncBrowserbase) -> None: extension = await async_client.extensions.create( file=b"raw file contents", ) - assert_matches_type(Extension, extension, path=["response"]) + assert_matches_type(ExtensionCreateResponse, extension, path=["response"]) @parametrize async def test_raw_response_create(self, async_client: AsyncBrowserbase) -> None: @@ -146,7 +146,7 @@ async def test_raw_response_create(self, async_client: AsyncBrowserbase) -> None assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" extension = await response.parse() - assert_matches_type(Extension, extension, path=["response"]) + assert_matches_type(ExtensionCreateResponse, extension, path=["response"]) @parametrize async def test_streaming_response_create(self, async_client: AsyncBrowserbase) -> None: @@ -157,7 +157,7 @@ async def test_streaming_response_create(self, async_client: AsyncBrowserbase) - assert response.http_request.headers.get("X-Stainless-Lang") == "python" extension = await response.parse() - assert_matches_type(Extension, extension, path=["response"]) + assert_matches_type(ExtensionCreateResponse, extension, path=["response"]) assert cast(Any, response.is_closed) is True @@ -166,7 +166,7 @@ async def test_method_retrieve(self, async_client: AsyncBrowserbase) -> None: extension = await async_client.extensions.retrieve( "id", ) - assert_matches_type(Extension, extension, path=["response"]) + assert_matches_type(ExtensionRetrieveResponse, extension, path=["response"]) @parametrize async def test_raw_response_retrieve(self, async_client: AsyncBrowserbase) -> None: @@ -177,7 +177,7 @@ async def test_raw_response_retrieve(self, async_client: AsyncBrowserbase) -> No assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" extension = await response.parse() - assert_matches_type(Extension, extension, path=["response"]) + assert_matches_type(ExtensionRetrieveResponse, extension, path=["response"]) @parametrize async def test_streaming_response_retrieve(self, async_client: AsyncBrowserbase) -> None: @@ -188,7 +188,7 @@ async def test_streaming_response_retrieve(self, async_client: AsyncBrowserbase) assert response.http_request.headers.get("X-Stainless-Lang") == "python" extension = await response.parse() - assert_matches_type(Extension, extension, path=["response"]) + assert_matches_type(ExtensionRetrieveResponse, extension, path=["response"]) assert cast(Any, response.is_closed) is True diff --git a/tests/api_resources/test_projects.py b/tests/api_resources/test_projects.py index c8241bf8..0d8e3c94 100644 --- a/tests/api_resources/test_projects.py +++ b/tests/api_resources/test_projects.py @@ -9,7 +9,7 @@ from browserbase import Browserbase, AsyncBrowserbase from tests.utils import assert_matches_type -from browserbase.types import Project, ProjectUsage, ProjectListResponse +from browserbase.types import ProjectListResponse, ProjectUsageResponse, ProjectRetrieveResponse base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") @@ -22,7 +22,7 @@ def test_method_retrieve(self, client: Browserbase) -> None: project = client.projects.retrieve( "id", ) - assert_matches_type(Project, project, path=["response"]) + assert_matches_type(ProjectRetrieveResponse, project, path=["response"]) @parametrize def test_raw_response_retrieve(self, client: Browserbase) -> None: @@ -33,7 +33,7 @@ def test_raw_response_retrieve(self, client: Browserbase) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" project = response.parse() - assert_matches_type(Project, project, path=["response"]) + assert_matches_type(ProjectRetrieveResponse, project, path=["response"]) @parametrize def test_streaming_response_retrieve(self, client: Browserbase) -> None: @@ -44,7 +44,7 @@ def test_streaming_response_retrieve(self, client: Browserbase) -> None: assert response.http_request.headers.get("X-Stainless-Lang") == "python" project = response.parse() - assert_matches_type(Project, project, path=["response"]) + assert_matches_type(ProjectRetrieveResponse, project, path=["response"]) assert cast(Any, response.is_closed) is True @@ -85,7 +85,7 @@ def test_method_usage(self, client: Browserbase) -> None: project = client.projects.usage( "id", ) - assert_matches_type(ProjectUsage, project, path=["response"]) + assert_matches_type(ProjectUsageResponse, project, path=["response"]) @parametrize def test_raw_response_usage(self, client: Browserbase) -> None: @@ -96,7 +96,7 @@ def test_raw_response_usage(self, client: Browserbase) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" project = response.parse() - assert_matches_type(ProjectUsage, project, path=["response"]) + assert_matches_type(ProjectUsageResponse, project, path=["response"]) @parametrize def test_streaming_response_usage(self, client: Browserbase) -> None: @@ -107,7 +107,7 @@ def test_streaming_response_usage(self, client: Browserbase) -> None: assert response.http_request.headers.get("X-Stainless-Lang") == "python" project = response.parse() - assert_matches_type(ProjectUsage, project, path=["response"]) + assert_matches_type(ProjectUsageResponse, project, path=["response"]) assert cast(Any, response.is_closed) is True @@ -129,7 +129,7 @@ async def test_method_retrieve(self, async_client: AsyncBrowserbase) -> None: project = await async_client.projects.retrieve( "id", ) - assert_matches_type(Project, project, path=["response"]) + assert_matches_type(ProjectRetrieveResponse, project, path=["response"]) @parametrize async def test_raw_response_retrieve(self, async_client: AsyncBrowserbase) -> None: @@ -140,7 +140,7 @@ async def test_raw_response_retrieve(self, async_client: AsyncBrowserbase) -> No assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" project = await response.parse() - assert_matches_type(Project, project, path=["response"]) + assert_matches_type(ProjectRetrieveResponse, project, path=["response"]) @parametrize async def test_streaming_response_retrieve(self, async_client: AsyncBrowserbase) -> None: @@ -151,7 +151,7 @@ async def test_streaming_response_retrieve(self, async_client: AsyncBrowserbase) assert response.http_request.headers.get("X-Stainless-Lang") == "python" project = await response.parse() - assert_matches_type(Project, project, path=["response"]) + assert_matches_type(ProjectRetrieveResponse, project, path=["response"]) assert cast(Any, response.is_closed) is True @@ -192,7 +192,7 @@ async def test_method_usage(self, async_client: AsyncBrowserbase) -> None: project = await async_client.projects.usage( "id", ) - assert_matches_type(ProjectUsage, project, path=["response"]) + assert_matches_type(ProjectUsageResponse, project, path=["response"]) @parametrize async def test_raw_response_usage(self, async_client: AsyncBrowserbase) -> None: @@ -203,7 +203,7 @@ async def test_raw_response_usage(self, async_client: AsyncBrowserbase) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" project = await response.parse() - assert_matches_type(ProjectUsage, project, path=["response"]) + assert_matches_type(ProjectUsageResponse, project, path=["response"]) @parametrize async def test_streaming_response_usage(self, async_client: AsyncBrowserbase) -> None: @@ -214,7 +214,7 @@ async def test_streaming_response_usage(self, async_client: AsyncBrowserbase) -> assert response.http_request.headers.get("X-Stainless-Lang") == "python" project = await response.parse() - assert_matches_type(ProjectUsage, project, path=["response"]) + assert_matches_type(ProjectUsageResponse, project, path=["response"]) assert cast(Any, response.is_closed) is True diff --git a/tests/api_resources/test_sessions.py b/tests/api_resources/test_sessions.py index 6a21aa85..7a16f64f 100644 --- a/tests/api_resources/test_sessions.py +++ b/tests/api_resources/test_sessions.py @@ -10,10 +10,10 @@ from browserbase import Browserbase, AsyncBrowserbase from tests.utils import assert_matches_type from browserbase.types import ( - Session, - SessionLiveURLs, SessionListResponse, + SessionDebugResponse, SessionCreateResponse, + SessionUpdateResponse, SessionRetrieveResponse, ) @@ -25,12 +25,15 @@ class TestSessions: @parametrize def test_method_create(self, client: Browserbase) -> None: - session = client.sessions.create() + session = client.sessions.create( + project_id="projectId", + ) assert_matches_type(SessionCreateResponse, session, path=["response"]) @parametrize def test_method_create_with_all_params(self, client: Browserbase) -> None: session = client.sessions.create( + project_id="projectId", browser_settings={ "advanced_stealth": True, "block_ads": True, @@ -41,6 +44,19 @@ def test_method_create_with_all_params(self, client: Browserbase) -> None: "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, "os": "windows", "record_session": True, @@ -52,8 +68,17 @@ def test_method_create_with_all_params(self, client: Browserbase) -> None: }, extension_id="extensionId", keep_alive=True, - project_id="projectId", - proxies=True, + proxies=[ + { + "type": "browserbase", + "domain_pattern": "domainPattern", + "geolocation": { + "country": "xx", + "city": "city", + "state": "xx", + }, + } + ], region="us-west-2", api_timeout=60, user_metadata={"foo": "bar"}, @@ -62,7 +87,9 @@ def test_method_create_with_all_params(self, client: Browserbase) -> None: @parametrize def test_raw_response_create(self, client: Browserbase) -> None: - response = client.sessions.with_raw_response.create() + response = client.sessions.with_raw_response.create( + project_id="projectId", + ) assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -71,7 +98,9 @@ def test_raw_response_create(self, client: Browserbase) -> None: @parametrize def test_streaming_response_create(self, client: Browserbase) -> None: - with client.sessions.with_streaming_response.create() as response: + with client.sessions.with_streaming_response.create( + project_id="projectId", + ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -122,42 +151,36 @@ def test_path_params_retrieve(self, client: Browserbase) -> None: def test_method_update(self, client: Browserbase) -> None: session = client.sessions.update( id="id", - status="REQUEST_RELEASE", - ) - assert_matches_type(Session, session, path=["response"]) - - @parametrize - def test_method_update_with_all_params(self, client: Browserbase) -> None: - session = client.sessions.update( - id="id", - status="REQUEST_RELEASE", project_id="projectId", + status="REQUEST_RELEASE", ) - assert_matches_type(Session, session, path=["response"]) + assert_matches_type(SessionUpdateResponse, session, path=["response"]) @parametrize def test_raw_response_update(self, client: Browserbase) -> None: response = client.sessions.with_raw_response.update( id="id", + project_id="projectId", status="REQUEST_RELEASE", ) assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" session = response.parse() - assert_matches_type(Session, session, path=["response"]) + assert_matches_type(SessionUpdateResponse, session, path=["response"]) @parametrize def test_streaming_response_update(self, client: Browserbase) -> None: with client.sessions.with_streaming_response.update( id="id", + project_id="projectId", status="REQUEST_RELEASE", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" session = response.parse() - assert_matches_type(Session, session, path=["response"]) + assert_matches_type(SessionUpdateResponse, session, path=["response"]) assert cast(Any, response.is_closed) is True @@ -166,6 +189,7 @@ def test_path_params_update(self, client: Browserbase) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): client.sessions.with_raw_response.update( id="", + project_id="projectId", status="REQUEST_RELEASE", ) @@ -207,7 +231,7 @@ def test_method_debug(self, client: Browserbase) -> None: session = client.sessions.debug( "id", ) - assert_matches_type(SessionLiveURLs, session, path=["response"]) + assert_matches_type(SessionDebugResponse, session, path=["response"]) @parametrize def test_raw_response_debug(self, client: Browserbase) -> None: @@ -218,7 +242,7 @@ def test_raw_response_debug(self, client: Browserbase) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" session = response.parse() - assert_matches_type(SessionLiveURLs, session, path=["response"]) + assert_matches_type(SessionDebugResponse, session, path=["response"]) @parametrize def test_streaming_response_debug(self, client: Browserbase) -> None: @@ -229,7 +253,7 @@ def test_streaming_response_debug(self, client: Browserbase) -> None: assert response.http_request.headers.get("X-Stainless-Lang") == "python" session = response.parse() - assert_matches_type(SessionLiveURLs, session, path=["response"]) + assert_matches_type(SessionDebugResponse, session, path=["response"]) assert cast(Any, response.is_closed) is True @@ -248,12 +272,15 @@ class TestAsyncSessions: @parametrize async def test_method_create(self, async_client: AsyncBrowserbase) -> None: - session = await async_client.sessions.create() + session = await async_client.sessions.create( + project_id="projectId", + ) assert_matches_type(SessionCreateResponse, session, path=["response"]) @parametrize async def test_method_create_with_all_params(self, async_client: AsyncBrowserbase) -> None: session = await async_client.sessions.create( + project_id="projectId", browser_settings={ "advanced_stealth": True, "block_ads": True, @@ -264,6 +291,19 @@ async def test_method_create_with_all_params(self, async_client: AsyncBrowserbas "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, "os": "windows", "record_session": True, @@ -275,8 +315,17 @@ async def test_method_create_with_all_params(self, async_client: AsyncBrowserbas }, extension_id="extensionId", keep_alive=True, - project_id="projectId", - proxies=True, + proxies=[ + { + "type": "browserbase", + "domain_pattern": "domainPattern", + "geolocation": { + "country": "xx", + "city": "city", + "state": "xx", + }, + } + ], region="us-west-2", api_timeout=60, user_metadata={"foo": "bar"}, @@ -285,7 +334,9 @@ async def test_method_create_with_all_params(self, async_client: AsyncBrowserbas @parametrize async def test_raw_response_create(self, async_client: AsyncBrowserbase) -> None: - response = await async_client.sessions.with_raw_response.create() + response = await async_client.sessions.with_raw_response.create( + project_id="projectId", + ) assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -294,7 +345,9 @@ async def test_raw_response_create(self, async_client: AsyncBrowserbase) -> None @parametrize async def test_streaming_response_create(self, async_client: AsyncBrowserbase) -> None: - async with async_client.sessions.with_streaming_response.create() as response: + async with async_client.sessions.with_streaming_response.create( + project_id="projectId", + ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -345,42 +398,36 @@ async def test_path_params_retrieve(self, async_client: AsyncBrowserbase) -> Non async def test_method_update(self, async_client: AsyncBrowserbase) -> None: session = await async_client.sessions.update( id="id", - status="REQUEST_RELEASE", - ) - assert_matches_type(Session, session, path=["response"]) - - @parametrize - async def test_method_update_with_all_params(self, async_client: AsyncBrowserbase) -> None: - session = await async_client.sessions.update( - id="id", - status="REQUEST_RELEASE", project_id="projectId", + status="REQUEST_RELEASE", ) - assert_matches_type(Session, session, path=["response"]) + assert_matches_type(SessionUpdateResponse, session, path=["response"]) @parametrize async def test_raw_response_update(self, async_client: AsyncBrowserbase) -> None: response = await async_client.sessions.with_raw_response.update( id="id", + project_id="projectId", status="REQUEST_RELEASE", ) assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" session = await response.parse() - assert_matches_type(Session, session, path=["response"]) + assert_matches_type(SessionUpdateResponse, session, path=["response"]) @parametrize async def test_streaming_response_update(self, async_client: AsyncBrowserbase) -> None: async with async_client.sessions.with_streaming_response.update( id="id", + project_id="projectId", status="REQUEST_RELEASE", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" session = await response.parse() - assert_matches_type(Session, session, path=["response"]) + assert_matches_type(SessionUpdateResponse, session, path=["response"]) assert cast(Any, response.is_closed) is True @@ -389,6 +436,7 @@ async def test_path_params_update(self, async_client: AsyncBrowserbase) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): await async_client.sessions.with_raw_response.update( id="", + project_id="projectId", status="REQUEST_RELEASE", ) @@ -430,7 +478,7 @@ async def test_method_debug(self, async_client: AsyncBrowserbase) -> None: session = await async_client.sessions.debug( "id", ) - assert_matches_type(SessionLiveURLs, session, path=["response"]) + assert_matches_type(SessionDebugResponse, session, path=["response"]) @parametrize async def test_raw_response_debug(self, async_client: AsyncBrowserbase) -> None: @@ -441,7 +489,7 @@ async def test_raw_response_debug(self, async_client: AsyncBrowserbase) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" session = await response.parse() - assert_matches_type(SessionLiveURLs, session, path=["response"]) + assert_matches_type(SessionDebugResponse, session, path=["response"]) @parametrize async def test_streaming_response_debug(self, async_client: AsyncBrowserbase) -> None: @@ -452,7 +500,7 @@ async def test_streaming_response_debug(self, async_client: AsyncBrowserbase) -> assert response.http_request.headers.get("X-Stainless-Lang") == "python" session = await response.parse() - assert_matches_type(SessionLiveURLs, session, path=["response"]) + assert_matches_type(SessionDebugResponse, session, path=["response"]) assert cast(Any, response.is_closed) is True diff --git a/tests/test_client.py b/tests/test_client.py index 71396d22..608cc1f9 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -864,7 +864,7 @@ def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter, clien respx_mock.post("/v1/sessions").mock(side_effect=httpx.TimeoutException("Test timeout error")) with pytest.raises(APITimeoutError): - client.sessions.with_streaming_response.create().__enter__() + client.sessions.with_streaming_response.create(project_id="projectId").__enter__() assert _get_open_connections(client) == 0 @@ -874,7 +874,7 @@ def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter, client respx_mock.post("/v1/sessions").mock(return_value=httpx.Response(500)) with pytest.raises(APIStatusError): - client.sessions.with_streaming_response.create().__enter__() + client.sessions.with_streaming_response.create(project_id="projectId").__enter__() assert _get_open_connections(client) == 0 @pytest.mark.parametrize("failures_before_success", [0, 2, 4]) @@ -903,7 +903,7 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: respx_mock.post("/v1/sessions").mock(side_effect=retry_handler) - response = client.sessions.with_raw_response.create() + response = client.sessions.with_raw_response.create(project_id="projectId") assert response.retries_taken == failures_before_success assert int(response.http_request.headers.get("x-stainless-retry-count")) == failures_before_success @@ -927,7 +927,9 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: respx_mock.post("/v1/sessions").mock(side_effect=retry_handler) - response = client.sessions.with_raw_response.create(extra_headers={"x-stainless-retry-count": Omit()}) + response = client.sessions.with_raw_response.create( + project_id="projectId", extra_headers={"x-stainless-retry-count": Omit()} + ) assert len(response.http_request.headers.get_list("x-stainless-retry-count")) == 0 @@ -950,7 +952,9 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: respx_mock.post("/v1/sessions").mock(side_effect=retry_handler) - response = client.sessions.with_raw_response.create(extra_headers={"x-stainless-retry-count": "42"}) + response = client.sessions.with_raw_response.create( + project_id="projectId", extra_headers={"x-stainless-retry-count": "42"} + ) assert response.http_request.headers.get("x-stainless-retry-count") == "42" @@ -1766,7 +1770,7 @@ async def test_retrying_timeout_errors_doesnt_leak( respx_mock.post("/v1/sessions").mock(side_effect=httpx.TimeoutException("Test timeout error")) with pytest.raises(APITimeoutError): - await async_client.sessions.with_streaming_response.create().__aenter__() + await async_client.sessions.with_streaming_response.create(project_id="projectId").__aenter__() assert _get_open_connections(async_client) == 0 @@ -1778,7 +1782,7 @@ async def test_retrying_status_errors_doesnt_leak( respx_mock.post("/v1/sessions").mock(return_value=httpx.Response(500)) with pytest.raises(APIStatusError): - await async_client.sessions.with_streaming_response.create().__aenter__() + await async_client.sessions.with_streaming_response.create(project_id="projectId").__aenter__() assert _get_open_connections(async_client) == 0 @pytest.mark.parametrize("failures_before_success", [0, 2, 4]) @@ -1807,7 +1811,7 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: respx_mock.post("/v1/sessions").mock(side_effect=retry_handler) - response = await client.sessions.with_raw_response.create() + response = await client.sessions.with_raw_response.create(project_id="projectId") assert response.retries_taken == failures_before_success assert int(response.http_request.headers.get("x-stainless-retry-count")) == failures_before_success @@ -1831,7 +1835,9 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: respx_mock.post("/v1/sessions").mock(side_effect=retry_handler) - response = await client.sessions.with_raw_response.create(extra_headers={"x-stainless-retry-count": Omit()}) + response = await client.sessions.with_raw_response.create( + project_id="projectId", extra_headers={"x-stainless-retry-count": Omit()} + ) assert len(response.http_request.headers.get_list("x-stainless-retry-count")) == 0 @@ -1854,7 +1860,9 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: respx_mock.post("/v1/sessions").mock(side_effect=retry_handler) - response = await client.sessions.with_raw_response.create(extra_headers={"x-stainless-retry-count": "42"}) + response = await client.sessions.with_raw_response.create( + project_id="projectId", extra_headers={"x-stainless-retry-count": "42"} + ) assert response.http_request.headers.get("x-stainless-retry-count") == "42" From 737cadfd3641933dde10337d96dac798a9957cdb Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 23 Feb 2026 22:56:14 +0000 Subject: [PATCH 242/330] feat(api): manual updates --- .stats.yml | 8 +- README.md | 1 - api.md | 29 ++-- src/browserbase/resources/contexts.py | 104 ++++++++++++-- src/browserbase/resources/extensions.py | 27 ++-- src/browserbase/resources/projects.py | 32 ++--- .../resources/sessions/sessions.py | 86 ++++++------ src/browserbase/types/__init__.py | 13 +- ...ontext_retrieve_response.py => context.py} | 4 +- .../types/context_create_params.py | 4 +- ...ension_create_response.py => extension.py} | 4 +- .../types/extension_retrieve_response.py | 22 --- ...roject_retrieve_response.py => project.py} | 4 +- .../types/project_list_response.py | 27 +--- ...ect_usage_response.py => project_usage.py} | 4 +- ...{session_update_response.py => session.py} | 4 +- .../types/session_create_params.py | 92 +++++------- .../types/session_list_response.py | 58 +------- ...debug_response.py => session_live_urls.py} | 4 +- .../types/session_update_params.py | 12 +- src/browserbase/types/sessions/__init__.py | 2 + .../types/sessions/log_list_response.py | 48 +------ .../sessions/recording_retrieve_response.py | 26 +--- src/browserbase/types/sessions/session_log.py | 46 ++++++ .../types/sessions/session_recording.py | 24 ++++ tests/api_resources/test_contexts.py | 120 +++++++++++++--- tests/api_resources/test_extensions.py | 26 ++-- tests/api_resources/test_projects.py | 26 ++-- tests/api_resources/test_sessions.py | 132 ++++++------------ tests/test_client.py | 28 ++-- 30 files changed, 503 insertions(+), 514 deletions(-) rename src/browserbase/types/{context_retrieve_response.py => context.py} (84%) rename src/browserbase/types/{extension_create_response.py => extension.py} (85%) delete mode 100644 src/browserbase/types/extension_retrieve_response.py rename src/browserbase/types/{project_retrieve_response.py => project.py} (87%) rename src/browserbase/types/{project_usage_response.py => project_usage.py} (78%) rename src/browserbase/types/{session_update_response.py => session.py} (95%) rename src/browserbase/types/{session_debug_response.py => session_live_urls.py} (88%) create mode 100644 src/browserbase/types/sessions/session_log.py create mode 100644 src/browserbase/types/sessions/session_recording.py diff --git a/.stats.yml b/.stats.yml index b000c8c1..9a94e549 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ -configured_endpoints: 18 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fbrowserbase-be7a4aeebb1605262935b4b3ab446a95b1fad8a7d18098943dd548c8a486ef13.yml -openapi_spec_hash: 1c950a109f80140711e7ae2cf87fddad -config_hash: b3ca4ec5b02e5333af51ebc2e9fdef1b +configured_endpoints: 19 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fbrowserbase-b92143ddb16135de4ff65ce8bcdfe9991d11c73570f42f07ea27e0da86209a44.yml +openapi_spec_hash: 16eb6e6c9687f01d2a791775b27dc315 +config_hash: b01d72cbe03bd762a73b05744086b2ec diff --git a/README.md b/README.md index dcb4b768..5d384354 100644 --- a/README.md +++ b/README.md @@ -122,7 +122,6 @@ from browserbase import Browserbase client = Browserbase() session = client.sessions.create( - project_id="projectId", browser_settings={}, ) print(session.browser_settings) diff --git a/api.md b/api.md index 01454851..d2d26e57 100644 --- a/api.md +++ b/api.md @@ -3,27 +3,28 @@ Types: ```python -from browserbase.types import ContextCreateResponse, ContextRetrieveResponse, ContextUpdateResponse +from browserbase.types import Context, ContextCreateResponse, ContextUpdateResponse ``` Methods: - client.contexts.create(\*\*params) -> ContextCreateResponse -- client.contexts.retrieve(id) -> ContextRetrieveResponse +- client.contexts.retrieve(id) -> Context - client.contexts.update(id) -> ContextUpdateResponse +- client.contexts.delete(id) -> None # Extensions Types: ```python -from browserbase.types import ExtensionCreateResponse, ExtensionRetrieveResponse +from browserbase.types import Extension ``` Methods: -- client.extensions.create(\*\*params) -> ExtensionCreateResponse -- client.extensions.retrieve(id) -> ExtensionRetrieveResponse +- client.extensions.create(\*\*params) -> Extension +- client.extensions.retrieve(id) -> Extension - client.extensions.delete(id) -> None # Projects @@ -31,14 +32,14 @@ Methods: Types: ```python -from browserbase.types import ProjectRetrieveResponse, ProjectListResponse, ProjectUsageResponse +from browserbase.types import Project, ProjectUsage, ProjectListResponse ``` Methods: -- client.projects.retrieve(id) -> ProjectRetrieveResponse +- client.projects.retrieve(id) -> Project - client.projects.list() -> ProjectListResponse -- client.projects.usage(id) -> ProjectUsageResponse +- client.projects.usage(id) -> ProjectUsage # Sessions @@ -46,11 +47,11 @@ Types: ```python from browserbase.types import ( + Session, + SessionLiveURLs, SessionCreateResponse, SessionRetrieveResponse, - SessionUpdateResponse, SessionListResponse, - SessionDebugResponse, ) ``` @@ -58,9 +59,9 @@ Methods: - client.sessions.create(\*\*params) -> SessionCreateResponse - client.sessions.retrieve(id) -> SessionRetrieveResponse -- client.sessions.update(id, \*\*params) -> SessionUpdateResponse +- client.sessions.update(id, \*\*params) -> Session - client.sessions.list(\*\*params) -> SessionListResponse -- client.sessions.debug(id) -> SessionDebugResponse +- client.sessions.debug(id) -> SessionLiveURLs ## Downloads @@ -73,7 +74,7 @@ Methods: Types: ```python -from browserbase.types.sessions import LogListResponse +from browserbase.types.sessions import SessionLog, LogListResponse ``` Methods: @@ -85,7 +86,7 @@ Methods: Types: ```python -from browserbase.types.sessions import RecordingRetrieveResponse +from browserbase.types.sessions import SessionRecording, RecordingRetrieveResponse ``` Methods: diff --git a/src/browserbase/resources/contexts.py b/src/browserbase/resources/contexts.py index d2bb4167..03af85f0 100644 --- a/src/browserbase/resources/contexts.py +++ b/src/browserbase/resources/contexts.py @@ -5,7 +5,7 @@ import httpx from ..types import context_create_params -from .._types import Body, Query, Headers, NotGiven, not_given +from .._types import Body, Omit, Query, Headers, NoneType, NotGiven, omit, not_given from .._utils import maybe_transform, async_maybe_transform from .._compat import cached_property from .._resource import SyncAPIResource, AsyncAPIResource @@ -16,9 +16,9 @@ async_to_streamed_response_wrapper, ) from .._base_client import make_request_options +from ..types.context import Context from ..types.context_create_response import ContextCreateResponse from ..types.context_update_response import ContextUpdateResponse -from ..types.context_retrieve_response import ContextRetrieveResponse __all__ = ["ContextsResource", "AsyncContextsResource"] @@ -46,7 +46,7 @@ def with_streaming_response(self) -> ContextsResourceWithStreamingResponse: def create( self, *, - project_id: str, + project_id: str | 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, @@ -89,9 +89,9 @@ def retrieve( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> ContextRetrieveResponse: + ) -> Context: """ - Get a Context + Context Args: extra_headers: Send extra headers @@ -109,7 +109,7 @@ def retrieve( options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=ContextRetrieveResponse, + cast_to=Context, ) def update( @@ -124,7 +124,7 @@ def update( timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> ContextUpdateResponse: """ - Update a Context + Update Context Args: extra_headers: Send extra headers @@ -145,6 +145,40 @@ def update( cast_to=ContextUpdateResponse, ) + def delete( + self, + 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, + ) -> None: + """ + Delete Context + + 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 id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + extra_headers = {"Accept": "*/*", **(extra_headers or {})} + return self._delete( + f"/v1/contexts/{id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=NoneType, + ) + class AsyncContextsResource(AsyncAPIResource): @cached_property @@ -169,7 +203,7 @@ def with_streaming_response(self) -> AsyncContextsResourceWithStreamingResponse: async def create( self, *, - project_id: str, + project_id: str | 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, @@ -212,9 +246,9 @@ async def retrieve( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> ContextRetrieveResponse: + ) -> Context: """ - Get a Context + Context Args: extra_headers: Send extra headers @@ -232,7 +266,7 @@ async def retrieve( options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=ContextRetrieveResponse, + cast_to=Context, ) async def update( @@ -247,7 +281,7 @@ async def update( timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> ContextUpdateResponse: """ - Update a Context + Update Context Args: extra_headers: Send extra headers @@ -268,6 +302,40 @@ async def update( cast_to=ContextUpdateResponse, ) + async def delete( + self, + 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, + ) -> None: + """ + Delete Context + + 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 id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + extra_headers = {"Accept": "*/*", **(extra_headers or {})} + return await self._delete( + f"/v1/contexts/{id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=NoneType, + ) + class ContextsResourceWithRawResponse: def __init__(self, contexts: ContextsResource) -> None: @@ -282,6 +350,9 @@ def __init__(self, contexts: ContextsResource) -> None: self.update = to_raw_response_wrapper( contexts.update, ) + self.delete = to_raw_response_wrapper( + contexts.delete, + ) class AsyncContextsResourceWithRawResponse: @@ -297,6 +368,9 @@ def __init__(self, contexts: AsyncContextsResource) -> None: self.update = async_to_raw_response_wrapper( contexts.update, ) + self.delete = async_to_raw_response_wrapper( + contexts.delete, + ) class ContextsResourceWithStreamingResponse: @@ -312,6 +386,9 @@ def __init__(self, contexts: ContextsResource) -> None: self.update = to_streamed_response_wrapper( contexts.update, ) + self.delete = to_streamed_response_wrapper( + contexts.delete, + ) class AsyncContextsResourceWithStreamingResponse: @@ -327,3 +404,6 @@ def __init__(self, contexts: AsyncContextsResource) -> None: self.update = async_to_streamed_response_wrapper( contexts.update, ) + self.delete = async_to_streamed_response_wrapper( + contexts.delete, + ) diff --git a/src/browserbase/resources/extensions.py b/src/browserbase/resources/extensions.py index 21d06e70..882a495f 100644 --- a/src/browserbase/resources/extensions.py +++ b/src/browserbase/resources/extensions.py @@ -18,8 +18,7 @@ async_to_streamed_response_wrapper, ) from .._base_client import make_request_options -from ..types.extension_create_response import ExtensionCreateResponse -from ..types.extension_retrieve_response import ExtensionRetrieveResponse +from ..types.extension import Extension __all__ = ["ExtensionsResource", "AsyncExtensionsResource"] @@ -54,7 +53,7 @@ def create( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> ExtensionCreateResponse: + ) -> Extension: """ Upload an Extension @@ -80,7 +79,7 @@ def create( options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=ExtensionCreateResponse, + cast_to=Extension, ) def retrieve( @@ -93,9 +92,9 @@ def retrieve( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> ExtensionRetrieveResponse: + ) -> Extension: """ - Get an Extension + Extension Args: extra_headers: Send extra headers @@ -113,7 +112,7 @@ def retrieve( options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=ExtensionRetrieveResponse, + cast_to=Extension, ) def delete( @@ -128,7 +127,7 @@ def delete( timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> None: """ - Delete an Extension + Delete Extension Args: extra_headers: Send extra headers @@ -181,7 +180,7 @@ async def create( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> ExtensionCreateResponse: + ) -> Extension: """ Upload an Extension @@ -207,7 +206,7 @@ async def create( options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=ExtensionCreateResponse, + cast_to=Extension, ) async def retrieve( @@ -220,9 +219,9 @@ async def retrieve( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> ExtensionRetrieveResponse: + ) -> Extension: """ - Get an Extension + Extension Args: extra_headers: Send extra headers @@ -240,7 +239,7 @@ async def retrieve( options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=ExtensionRetrieveResponse, + cast_to=Extension, ) async def delete( @@ -255,7 +254,7 @@ async def delete( timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> None: """ - Delete an Extension + Delete Extension Args: extra_headers: Send extra headers diff --git a/src/browserbase/resources/projects.py b/src/browserbase/resources/projects.py index 62c28afa..a6ae6338 100644 --- a/src/browserbase/resources/projects.py +++ b/src/browserbase/resources/projects.py @@ -14,9 +14,9 @@ async_to_streamed_response_wrapper, ) from .._base_client import make_request_options +from ..types.project import Project +from ..types.project_usage import ProjectUsage from ..types.project_list_response import ProjectListResponse -from ..types.project_usage_response import ProjectUsageResponse -from ..types.project_retrieve_response import ProjectRetrieveResponse __all__ = ["ProjectsResource", "AsyncProjectsResource"] @@ -51,9 +51,9 @@ def retrieve( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> ProjectRetrieveResponse: + ) -> Project: """ - Get a Project + Project Args: extra_headers: Send extra headers @@ -71,7 +71,7 @@ def retrieve( options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=ProjectRetrieveResponse, + cast_to=Project, ) def list( @@ -84,7 +84,7 @@ def list( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> ProjectListResponse: - """List Projects""" + """List projects""" return self._get( "/v1/projects", options=make_request_options( @@ -103,9 +103,9 @@ def usage( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> ProjectUsageResponse: + ) -> ProjectUsage: """ - Get Project Usage + Project Usage Args: extra_headers: Send extra headers @@ -123,7 +123,7 @@ def usage( options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=ProjectUsageResponse, + cast_to=ProjectUsage, ) @@ -157,9 +157,9 @@ async def retrieve( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> ProjectRetrieveResponse: + ) -> Project: """ - Get a Project + Project Args: extra_headers: Send extra headers @@ -177,7 +177,7 @@ async def retrieve( options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=ProjectRetrieveResponse, + cast_to=Project, ) async def list( @@ -190,7 +190,7 @@ async def list( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> ProjectListResponse: - """List Projects""" + """List projects""" return await self._get( "/v1/projects", options=make_request_options( @@ -209,9 +209,9 @@ async def usage( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> ProjectUsageResponse: + ) -> ProjectUsage: """ - Get Project Usage + Project Usage Args: extra_headers: Send extra headers @@ -229,7 +229,7 @@ async def usage( options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=ProjectUsageResponse, + cast_to=ProjectUsage, ) diff --git a/src/browserbase/resources/sessions/sessions.py b/src/browserbase/resources/sessions/sessions.py index ceaaeb81..09fd15a3 100644 --- a/src/browserbase/resources/sessions/sessions.py +++ b/src/browserbase/resources/sessions/sessions.py @@ -51,10 +51,10 @@ async_to_streamed_response_wrapper, ) from ..._base_client import make_request_options +from ...types.session import Session +from ...types.session_live_urls import SessionLiveURLs from ...types.session_list_response import SessionListResponse -from ...types.session_debug_response import SessionDebugResponse from ...types.session_create_response import SessionCreateResponse -from ...types.session_update_response import SessionUpdateResponse from ...types.session_retrieve_response import SessionRetrieveResponse __all__ = ["SessionsResource", "AsyncSessionsResource"] @@ -99,11 +99,11 @@ def with_streaming_response(self) -> SessionsResourceWithStreamingResponse: def create( self, *, - project_id: str, browser_settings: session_create_params.BrowserSettings | Omit = omit, extension_id: str | Omit = omit, keep_alive: bool | Omit = omit, - proxies: Union[Iterable[session_create_params.ProxiesUnionMember0], bool] | Omit = omit, + project_id: str | Omit = omit, + proxies: Union[bool, Iterable[session_create_params.ProxiesUnionMember1]] | Omit = omit, region: Literal["us-west-2", "us-east-1", "eu-central-1", "ap-southeast-1"] | Omit = omit, api_timeout: int | Omit = omit, user_metadata: Dict[str, object] | Omit = omit, @@ -117,17 +117,17 @@ def create( """Create a Session Args: - project_id: The Project ID. + extension_id: The uploaded Extension ID. - Can be found in - [Settings](https://www.browserbase.com/settings). - - extension_id: The uploaded Extension ID. See + See [Upload Extension](/reference/api/upload-an-extension). keep_alive: Set to true to keep the session alive even after disconnections. Available on the Hobby Plan and above. + project_id: The Project ID. Can be found in + [Settings](https://www.browserbase.com/settings). + proxies: Proxy configuration. Can be true for default proxy, or an array of proxy configurations. @@ -151,10 +151,10 @@ def create( "/v1/sessions", body=maybe_transform( { - "project_id": project_id, "browser_settings": browser_settings, "extension_id": extension_id, "keep_alive": keep_alive, + "project_id": project_id, "proxies": proxies, "region": region, "api_timeout": api_timeout, @@ -180,7 +180,7 @@ def retrieve( timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> SessionRetrieveResponse: """ - Get a Session + Session Args: extra_headers: Send extra headers @@ -205,26 +205,25 @@ def update( self, id: str, *, - project_id: str, status: Literal["REQUEST_RELEASE"], + project_id: str | 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, - ) -> SessionUpdateResponse: - """Update a Session + ) -> Session: + """ + Update Session Args: - project_id: The Project ID. - - Can be found in - [Settings](https://www.browserbase.com/settings). - status: Set to `REQUEST_RELEASE` to request that the session complete. Use before session's timeout to avoid additional charges. + project_id: The Project ID. Can be found in + [Settings](https://www.browserbase.com/settings). + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -239,15 +238,15 @@ def update( f"/v1/sessions/{id}", body=maybe_transform( { - "project_id": project_id, "status": status, + "project_id": project_id, }, session_update_params.SessionUpdateParams, ), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=SessionUpdateResponse, + cast_to=Session, ) def list( @@ -307,7 +306,7 @@ def debug( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> SessionDebugResponse: + ) -> SessionLiveURLs: """ Session Live URLs @@ -327,7 +326,7 @@ def debug( options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=SessionDebugResponse, + cast_to=SessionLiveURLs, ) @@ -370,11 +369,11 @@ def with_streaming_response(self) -> AsyncSessionsResourceWithStreamingResponse: async def create( self, *, - project_id: str, browser_settings: session_create_params.BrowserSettings | Omit = omit, extension_id: str | Omit = omit, keep_alive: bool | Omit = omit, - proxies: Union[Iterable[session_create_params.ProxiesUnionMember0], bool] | Omit = omit, + project_id: str | Omit = omit, + proxies: Union[bool, Iterable[session_create_params.ProxiesUnionMember1]] | Omit = omit, region: Literal["us-west-2", "us-east-1", "eu-central-1", "ap-southeast-1"] | Omit = omit, api_timeout: int | Omit = omit, user_metadata: Dict[str, object] | Omit = omit, @@ -388,17 +387,17 @@ async def create( """Create a Session Args: - project_id: The Project ID. + extension_id: The uploaded Extension ID. - Can be found in - [Settings](https://www.browserbase.com/settings). - - extension_id: The uploaded Extension ID. See + See [Upload Extension](/reference/api/upload-an-extension). keep_alive: Set to true to keep the session alive even after disconnections. Available on the Hobby Plan and above. + project_id: The Project ID. Can be found in + [Settings](https://www.browserbase.com/settings). + proxies: Proxy configuration. Can be true for default proxy, or an array of proxy configurations. @@ -422,10 +421,10 @@ async def create( "/v1/sessions", body=await async_maybe_transform( { - "project_id": project_id, "browser_settings": browser_settings, "extension_id": extension_id, "keep_alive": keep_alive, + "project_id": project_id, "proxies": proxies, "region": region, "api_timeout": api_timeout, @@ -451,7 +450,7 @@ async def retrieve( timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> SessionRetrieveResponse: """ - Get a Session + Session Args: extra_headers: Send extra headers @@ -476,26 +475,25 @@ async def update( self, id: str, *, - project_id: str, status: Literal["REQUEST_RELEASE"], + project_id: str | 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, - ) -> SessionUpdateResponse: - """Update a Session + ) -> Session: + """ + Update Session Args: - project_id: The Project ID. - - Can be found in - [Settings](https://www.browserbase.com/settings). - status: Set to `REQUEST_RELEASE` to request that the session complete. Use before session's timeout to avoid additional charges. + project_id: The Project ID. Can be found in + [Settings](https://www.browserbase.com/settings). + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -510,15 +508,15 @@ async def update( f"/v1/sessions/{id}", body=await async_maybe_transform( { - "project_id": project_id, "status": status, + "project_id": project_id, }, session_update_params.SessionUpdateParams, ), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=SessionUpdateResponse, + cast_to=Session, ) async def list( @@ -578,7 +576,7 @@ async def debug( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> SessionDebugResponse: + ) -> SessionLiveURLs: """ Session Live URLs @@ -598,7 +596,7 @@ async def debug( options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=SessionDebugResponse, + cast_to=SessionLiveURLs, ) diff --git a/src/browserbase/types/__init__.py b/src/browserbase/types/__init__.py index 20e2f905..4dd85ddb 100644 --- a/src/browserbase/types/__init__.py +++ b/src/browserbase/types/__init__.py @@ -2,21 +2,20 @@ from __future__ import annotations +from .context import Context as Context +from .project import Project as Project +from .session import Session as Session +from .extension import Extension as Extension +from .project_usage import ProjectUsage as ProjectUsage +from .session_live_urls import SessionLiveURLs as SessionLiveURLs from .session_list_params import SessionListParams as SessionListParams from .context_create_params import ContextCreateParams as ContextCreateParams from .project_list_response import ProjectListResponse as ProjectListResponse from .session_create_params import SessionCreateParams as SessionCreateParams from .session_list_response import SessionListResponse as SessionListResponse from .session_update_params import SessionUpdateParams as SessionUpdateParams -from .project_usage_response import ProjectUsageResponse as ProjectUsageResponse -from .session_debug_response import SessionDebugResponse as SessionDebugResponse from .context_create_response import ContextCreateResponse as ContextCreateResponse from .context_update_response import ContextUpdateResponse as ContextUpdateResponse from .extension_create_params import ExtensionCreateParams as ExtensionCreateParams from .session_create_response import SessionCreateResponse as SessionCreateResponse -from .session_update_response import SessionUpdateResponse as SessionUpdateResponse -from .context_retrieve_response import ContextRetrieveResponse as ContextRetrieveResponse -from .extension_create_response import ExtensionCreateResponse as ExtensionCreateResponse -from .project_retrieve_response import ProjectRetrieveResponse as ProjectRetrieveResponse from .session_retrieve_response import SessionRetrieveResponse as SessionRetrieveResponse -from .extension_retrieve_response import ExtensionRetrieveResponse as ExtensionRetrieveResponse diff --git a/src/browserbase/types/context_retrieve_response.py b/src/browserbase/types/context.py similarity index 84% rename from src/browserbase/types/context_retrieve_response.py rename to src/browserbase/types/context.py index c2cd6925..cb5c32fd 100644 --- a/src/browserbase/types/context_retrieve_response.py +++ b/src/browserbase/types/context.py @@ -6,10 +6,10 @@ from .._models import BaseModel -__all__ = ["ContextRetrieveResponse"] +__all__ = ["Context"] -class ContextRetrieveResponse(BaseModel): +class Context(BaseModel): id: str created_at: datetime = FieldInfo(alias="createdAt") diff --git a/src/browserbase/types/context_create_params.py b/src/browserbase/types/context_create_params.py index 75cd1fcd..66c6c468 100644 --- a/src/browserbase/types/context_create_params.py +++ b/src/browserbase/types/context_create_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,7 +10,7 @@ class ContextCreateParams(TypedDict, total=False): - project_id: Required[Annotated[str, PropertyInfo(alias="projectId")]] + project_id: Annotated[str, PropertyInfo(alias="projectId")] """The Project ID. Can be found in [Settings](https://www.browserbase.com/settings). diff --git a/src/browserbase/types/extension_create_response.py b/src/browserbase/types/extension.py similarity index 85% rename from src/browserbase/types/extension_create_response.py rename to src/browserbase/types/extension.py index d2b74f41..94582c34 100644 --- a/src/browserbase/types/extension_create_response.py +++ b/src/browserbase/types/extension.py @@ -6,10 +6,10 @@ from .._models import BaseModel -__all__ = ["ExtensionCreateResponse"] +__all__ = ["Extension"] -class ExtensionCreateResponse(BaseModel): +class Extension(BaseModel): id: str created_at: datetime = FieldInfo(alias="createdAt") diff --git a/src/browserbase/types/extension_retrieve_response.py b/src/browserbase/types/extension_retrieve_response.py deleted file mode 100644 index c786348e..00000000 --- a/src/browserbase/types/extension_retrieve_response.py +++ /dev/null @@ -1,22 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from datetime import datetime - -from pydantic import Field as FieldInfo - -from .._models import BaseModel - -__all__ = ["ExtensionRetrieveResponse"] - - -class ExtensionRetrieveResponse(BaseModel): - id: str - - created_at: datetime = FieldInfo(alias="createdAt") - - file_name: str = FieldInfo(alias="fileName") - - project_id: str = FieldInfo(alias="projectId") - """The Project ID linked to the uploaded Extension.""" - - updated_at: datetime = FieldInfo(alias="updatedAt") diff --git a/src/browserbase/types/project_retrieve_response.py b/src/browserbase/types/project.py similarity index 87% rename from src/browserbase/types/project_retrieve_response.py rename to src/browserbase/types/project.py index 78126679..dc3cf335 100644 --- a/src/browserbase/types/project_retrieve_response.py +++ b/src/browserbase/types/project.py @@ -6,10 +6,10 @@ from .._models import BaseModel -__all__ = ["ProjectRetrieveResponse"] +__all__ = ["Project"] -class ProjectRetrieveResponse(BaseModel): +class Project(BaseModel): id: str concurrency: int diff --git a/src/browserbase/types/project_list_response.py b/src/browserbase/types/project_list_response.py index e364b520..2d05a236 100644 --- a/src/browserbase/types/project_list_response.py +++ b/src/browserbase/types/project_list_response.py @@ -1,31 +1,10 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. from typing import List -from datetime import datetime from typing_extensions import TypeAlias -from pydantic import Field as FieldInfo +from .project import Project -from .._models import BaseModel +__all__ = ["ProjectListResponse"] -__all__ = ["ProjectListResponse", "ProjectListResponseItem"] - - -class ProjectListResponseItem(BaseModel): - id: str - - concurrency: int - """The maximum number of sessions that this project can run concurrently.""" - - created_at: datetime = FieldInfo(alias="createdAt") - - default_timeout: int = FieldInfo(alias="defaultTimeout") - - name: str - - owner_id: str = FieldInfo(alias="ownerId") - - updated_at: datetime = FieldInfo(alias="updatedAt") - - -ProjectListResponse: TypeAlias = List[ProjectListResponseItem] +ProjectListResponse: TypeAlias = List[Project] diff --git a/src/browserbase/types/project_usage_response.py b/src/browserbase/types/project_usage.py similarity index 78% rename from src/browserbase/types/project_usage_response.py rename to src/browserbase/types/project_usage.py index b52fccfe..c8a03f5b 100644 --- a/src/browserbase/types/project_usage_response.py +++ b/src/browserbase/types/project_usage.py @@ -4,10 +4,10 @@ from .._models import BaseModel -__all__ = ["ProjectUsageResponse"] +__all__ = ["ProjectUsage"] -class ProjectUsageResponse(BaseModel): +class ProjectUsage(BaseModel): browser_minutes: int = FieldInfo(alias="browserMinutes") proxy_bytes: int = FieldInfo(alias="proxyBytes") diff --git a/src/browserbase/types/session_update_response.py b/src/browserbase/types/session.py similarity index 95% rename from src/browserbase/types/session_update_response.py rename to src/browserbase/types/session.py index 67a13711..16450e29 100644 --- a/src/browserbase/types/session_update_response.py +++ b/src/browserbase/types/session.py @@ -8,10 +8,10 @@ from .._models import BaseModel -__all__ = ["SessionUpdateResponse"] +__all__ = ["Session"] -class SessionUpdateResponse(BaseModel): +class Session(BaseModel): id: str created_at: datetime = FieldInfo(alias="createdAt") diff --git a/src/browserbase/types/session_create_params.py b/src/browserbase/types/session_create_params.py index 2ba36400..33dd6fd5 100644 --- a/src/browserbase/types/session_create_params.py +++ b/src/browserbase/types/session_create_params.py @@ -2,33 +2,25 @@ from __future__ import annotations -from typing import Dict, List, Union, Iterable +from typing import Dict, Union, Iterable from typing_extensions import Literal, Required, Annotated, TypeAlias, TypedDict -from .._types import SequenceNotStr from .._utils import PropertyInfo __all__ = [ "SessionCreateParams", "BrowserSettings", "BrowserSettingsContext", - "BrowserSettingsFingerprint", - "BrowserSettingsFingerprintScreen", "BrowserSettingsViewport", - "ProxiesUnionMember0", - "ProxiesUnionMember0UnionMember0", - "ProxiesUnionMember0UnionMember0Geolocation", - "ProxiesUnionMember0UnionMember1", + "ProxiesUnionMember1", + "ProxiesUnionMember1BrowserbaseProxyConfig", + "ProxiesUnionMember1BrowserbaseProxyConfigGeolocation", + "ProxiesUnionMember1ExternalProxyConfig", + "ProxiesUnionMember1NoneProxyConfig", ] class SessionCreateParams(TypedDict, total=False): - project_id: Required[Annotated[str, PropertyInfo(alias="projectId")]] - """The Project ID. - - Can be found in [Settings](https://www.browserbase.com/settings). - """ - browser_settings: Annotated[BrowserSettings, PropertyInfo(alias="browserSettings")] extension_id: Annotated[str, PropertyInfo(alias="extensionId")] @@ -43,7 +35,13 @@ class SessionCreateParams(TypedDict, total=False): Available on the Hobby Plan and above. """ - proxies: Union[Iterable[ProxiesUnionMember0], bool] + project_id: Annotated[str, PropertyInfo(alias="projectId")] + """The Project ID. + + Can be found in [Settings](https://www.browserbase.com/settings). + """ + + proxies: Union[bool, Iterable[ProxiesUnionMember1]] """Proxy configuration. Can be true for default proxy, or an array of proxy configurations. @@ -74,42 +72,10 @@ class BrowserSettingsContext(TypedDict, total=False): """Whether or not to persist the context after browsing. Defaults to `false`.""" -class BrowserSettingsFingerprintScreen(TypedDict, total=False): - max_height: Annotated[int, PropertyInfo(alias="maxHeight")] - - max_width: Annotated[int, PropertyInfo(alias="maxWidth")] - - min_height: Annotated[int, PropertyInfo(alias="minHeight")] - - min_width: Annotated[int, PropertyInfo(alias="minWidth")] - - -class BrowserSettingsFingerprint(TypedDict, total=False): - """ - See usage examples [on the Stealth Mode page](/features/stealth-mode#fingerprinting) - """ - - 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: BrowserSettingsFingerprintScreen - - class BrowserSettingsViewport(TypedDict, total=False): height: int - """The height of the browser.""" width: int - """The width of the browser.""" class BrowserSettings(TypedDict, total=False): @@ -139,12 +105,6 @@ class BrowserSettings(TypedDict, total=False): See [Upload Extension](/reference/api/upload-an-extension). """ - fingerprint: BrowserSettingsFingerprint - """ - See usage examples - [on the Stealth Mode page](/features/stealth-mode#fingerprinting) - """ - log_session: Annotated[bool, PropertyInfo(alias="logSession")] """Enable or disable session logging. Defaults to `true`.""" @@ -163,8 +123,8 @@ class BrowserSettings(TypedDict, total=False): viewport: BrowserSettingsViewport -class ProxiesUnionMember0UnionMember0Geolocation(TypedDict, total=False): - """Geographic location for the proxy. Optional.""" +class ProxiesUnionMember1BrowserbaseProxyConfigGeolocation(TypedDict, total=False): + """Configuration for geolocation""" country: Required[str] """Country code in ISO 3166-1 alpha-2 format""" @@ -176,7 +136,7 @@ class ProxiesUnionMember0UnionMember0Geolocation(TypedDict, total=False): """US state code (2 characters). Must also specify US as the country. Optional.""" -class ProxiesUnionMember0UnionMember0(TypedDict, total=False): +class ProxiesUnionMember1BrowserbaseProxyConfig(TypedDict, total=False): type: Required[Literal["browserbase"]] """Type of proxy. @@ -189,11 +149,11 @@ class ProxiesUnionMember0UnionMember0(TypedDict, total=False): If omitted, defaults to all domains. Optional. """ - geolocation: ProxiesUnionMember0UnionMember0Geolocation - """Geographic location for the proxy. Optional.""" + geolocation: ProxiesUnionMember1BrowserbaseProxyConfigGeolocation + """Configuration for geolocation""" -class ProxiesUnionMember0UnionMember1(TypedDict, total=False): +class ProxiesUnionMember1ExternalProxyConfig(TypedDict, total=False): server: Required[str] """Server URL for external proxy. Required.""" @@ -213,4 +173,16 @@ class ProxiesUnionMember0UnionMember1(TypedDict, total=False): """Username for external proxy authentication. Optional.""" -ProxiesUnionMember0: TypeAlias = Union[ProxiesUnionMember0UnionMember0, ProxiesUnionMember0UnionMember1] +class ProxiesUnionMember1NoneProxyConfig(TypedDict, total=False): + domain_pattern: Required[Annotated[str, PropertyInfo(alias="domainPattern")]] + """Domain pattern for which site should have proxies disabled.""" + + type: Required[Literal["none"]] + """Type of proxy. Use 'none' to disable proxy for matching domains.""" + + +ProxiesUnionMember1: TypeAlias = Union[ + ProxiesUnionMember1BrowserbaseProxyConfig, + ProxiesUnionMember1ExternalProxyConfig, + ProxiesUnionMember1NoneProxyConfig, +] diff --git a/src/browserbase/types/session_list_response.py b/src/browserbase/types/session_list_response.py index 4c1bd885..ca162ddb 100644 --- a/src/browserbase/types/session_list_response.py +++ b/src/browserbase/types/session_list_response.py @@ -1,58 +1,10 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -from typing import Dict, List, Optional -from datetime import datetime -from typing_extensions import Literal, TypeAlias +from typing import List +from typing_extensions import TypeAlias -from pydantic import Field as FieldInfo +from .session import Session -from .._models import BaseModel +__all__ = ["SessionListResponse"] -__all__ = ["SessionListResponse", "SessionListResponseItem"] - - -class SessionListResponseItem(BaseModel): - id: str - - created_at: datetime = FieldInfo(alias="createdAt") - - expires_at: datetime = FieldInfo(alias="expiresAt") - - keep_alive: bool = FieldInfo(alias="keepAlive") - """Indicates if the Session was created to be kept alive upon disconnections""" - - project_id: str = FieldInfo(alias="projectId") - """The Project ID linked to the Session.""" - - proxy_bytes: int = FieldInfo(alias="proxyBytes") - """Bytes used via the [Proxy](/features/stealth-mode#proxies-and-residential-ips)""" - - region: Literal["us-west-2", "us-east-1", "eu-central-1", "ap-southeast-1"] - """The region where the Session is running.""" - - started_at: datetime = FieldInfo(alias="startedAt") - - status: Literal["RUNNING", "ERROR", "TIMED_OUT", "COMPLETED"] - - updated_at: datetime = FieldInfo(alias="updatedAt") - - avg_cpu_usage: Optional[int] = FieldInfo(alias="avgCpuUsage", default=None) - """CPU used by the Session""" - - context_id: Optional[str] = FieldInfo(alias="contextId", default=None) - """Optional. The Context linked to the Session.""" - - ended_at: Optional[datetime] = FieldInfo(alias="endedAt", default=None) - - memory_usage: Optional[int] = FieldInfo(alias="memoryUsage", default=None) - """Memory used by the Session""" - - user_metadata: Optional[Dict[str, object]] = FieldInfo(alias="userMetadata", default=None) - """Arbitrary user metadata to attach to the session. - - To learn more about user metadata, see - [User Metadata](/features/sessions#user-metadata). - """ - - -SessionListResponse: TypeAlias = List[SessionListResponseItem] +SessionListResponse: TypeAlias = List[Session] diff --git a/src/browserbase/types/session_debug_response.py b/src/browserbase/types/session_live_urls.py similarity index 88% rename from src/browserbase/types/session_debug_response.py rename to src/browserbase/types/session_live_urls.py index 9cee7a77..3c7ba320 100644 --- a/src/browserbase/types/session_debug_response.py +++ b/src/browserbase/types/session_live_urls.py @@ -6,7 +6,7 @@ from .._models import BaseModel -__all__ = ["SessionDebugResponse", "Page"] +__all__ = ["SessionLiveURLs", "Page"] class Page(BaseModel): @@ -23,7 +23,7 @@ class Page(BaseModel): url: str -class SessionDebugResponse(BaseModel): +class SessionLiveURLs(BaseModel): debugger_fullscreen_url: str = FieldInfo(alias="debuggerFullscreenUrl") debugger_url: str = FieldInfo(alias="debuggerUrl") diff --git a/src/browserbase/types/session_update_params.py b/src/browserbase/types/session_update_params.py index 66dcd351..71c589d9 100644 --- a/src/browserbase/types/session_update_params.py +++ b/src/browserbase/types/session_update_params.py @@ -10,14 +10,14 @@ class SessionUpdateParams(TypedDict, total=False): - project_id: Required[Annotated[str, PropertyInfo(alias="projectId")]] - """The Project ID. - - Can be found in [Settings](https://www.browserbase.com/settings). - """ - status: Required[Literal["REQUEST_RELEASE"]] """Set to `REQUEST_RELEASE` to request that the session complete. Use before session's timeout to avoid additional charges. """ + + project_id: Annotated[str, PropertyInfo(alias="projectId")] + """The Project ID. + + Can be found in [Settings](https://www.browserbase.com/settings). + """ diff --git a/src/browserbase/types/sessions/__init__.py b/src/browserbase/types/sessions/__init__.py index 69d54703..0cef6b19 100644 --- a/src/browserbase/types/sessions/__init__.py +++ b/src/browserbase/types/sessions/__init__.py @@ -2,7 +2,9 @@ from __future__ import annotations +from .session_log import SessionLog as SessionLog from .log_list_response import LogListResponse as LogListResponse +from .session_recording import SessionRecording as SessionRecording from .upload_create_params import UploadCreateParams as UploadCreateParams from .upload_create_response import UploadCreateResponse as UploadCreateResponse from .recording_retrieve_response import RecordingRetrieveResponse as RecordingRetrieveResponse diff --git a/src/browserbase/types/sessions/log_list_response.py b/src/browserbase/types/sessions/log_list_response.py index efd848ab..2b325a8c 100644 --- a/src/browserbase/types/sessions/log_list_response.py +++ b/src/browserbase/types/sessions/log_list_response.py @@ -1,50 +1,10 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -from typing import Dict, List, Optional +from typing import List from typing_extensions import TypeAlias -from pydantic import Field as FieldInfo +from .session_log import SessionLog -from ..._models import BaseModel +__all__ = ["LogListResponse"] -__all__ = ["LogListResponse", "LogListResponseItem", "LogListResponseItemRequest", "LogListResponseItemResponse"] - - -class LogListResponseItemRequest(BaseModel): - params: Dict[str, object] - - raw_body: str = FieldInfo(alias="rawBody") - - timestamp: Optional[int] = None - """milliseconds that have elapsed since the UNIX epoch""" - - -class LogListResponseItemResponse(BaseModel): - raw_body: str = FieldInfo(alias="rawBody") - - result: Dict[str, object] - - timestamp: Optional[int] = None - """milliseconds that have elapsed since the UNIX epoch""" - - -class LogListResponseItem(BaseModel): - method: str - - page_id: int = FieldInfo(alias="pageId") - - session_id: str = FieldInfo(alias="sessionId") - - frame_id: Optional[str] = FieldInfo(alias="frameId", default=None) - - loader_id: Optional[str] = FieldInfo(alias="loaderId", default=None) - - request: Optional[LogListResponseItemRequest] = None - - response: Optional[LogListResponseItemResponse] = None - - timestamp: Optional[int] = None - """milliseconds that have elapsed since the UNIX epoch""" - - -LogListResponse: TypeAlias = List[LogListResponseItem] +LogListResponse: TypeAlias = List[SessionLog] diff --git a/src/browserbase/types/sessions/recording_retrieve_response.py b/src/browserbase/types/sessions/recording_retrieve_response.py index d3613b8c..951969bb 100644 --- a/src/browserbase/types/sessions/recording_retrieve_response.py +++ b/src/browserbase/types/sessions/recording_retrieve_response.py @@ -1,28 +1,10 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -from typing import Dict, List +from typing import List from typing_extensions import TypeAlias -from pydantic import Field as FieldInfo +from .session_recording import SessionRecording -from ..._models import BaseModel +__all__ = ["RecordingRetrieveResponse"] -__all__ = ["RecordingRetrieveResponse", "RecordingRetrieveResponseItem"] - - -class RecordingRetrieveResponseItem(BaseModel): - data: Dict[str, object] - """ - See - [rrweb documentation](https://github.com/rrweb-io/rrweb/blob/master/docs/recipes/dive-into-event.md). - """ - - session_id: str = FieldInfo(alias="sessionId") - - timestamp: int - """milliseconds that have elapsed since the UNIX epoch""" - - type: int - - -RecordingRetrieveResponse: TypeAlias = List[RecordingRetrieveResponseItem] +RecordingRetrieveResponse: TypeAlias = List[SessionRecording] diff --git a/src/browserbase/types/sessions/session_log.py b/src/browserbase/types/sessions/session_log.py new file mode 100644 index 00000000..428f518a --- /dev/null +++ b/src/browserbase/types/sessions/session_log.py @@ -0,0 +1,46 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Dict, Optional + +from pydantic import Field as FieldInfo + +from ..._models import BaseModel + +__all__ = ["SessionLog", "Request", "Response"] + + +class Request(BaseModel): + params: Dict[str, object] + + raw_body: str = FieldInfo(alias="rawBody") + + timestamp: Optional[int] = None + """milliseconds that have elapsed since the UNIX epoch""" + + +class Response(BaseModel): + raw_body: str = FieldInfo(alias="rawBody") + + result: Dict[str, object] + + timestamp: Optional[int] = None + """milliseconds that have elapsed since the UNIX epoch""" + + +class SessionLog(BaseModel): + method: str + + page_id: int = FieldInfo(alias="pageId") + + session_id: str = FieldInfo(alias="sessionId") + + frame_id: Optional[str] = FieldInfo(alias="frameId", default=None) + + loader_id: Optional[str] = FieldInfo(alias="loaderId", default=None) + + request: Optional[Request] = None + + response: Optional[Response] = None + + timestamp: Optional[int] = None + """milliseconds that have elapsed since the UNIX epoch""" diff --git a/src/browserbase/types/sessions/session_recording.py b/src/browserbase/types/sessions/session_recording.py new file mode 100644 index 00000000..c8471371 --- /dev/null +++ b/src/browserbase/types/sessions/session_recording.py @@ -0,0 +1,24 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Dict + +from pydantic import Field as FieldInfo + +from ..._models import BaseModel + +__all__ = ["SessionRecording"] + + +class SessionRecording(BaseModel): + data: Dict[str, object] + """ + See + [rrweb documentation](https://github.com/rrweb-io/rrweb/blob/master/docs/recipes/dive-into-event.md). + """ + + session_id: str = FieldInfo(alias="sessionId") + + timestamp: int + """milliseconds that have elapsed since the UNIX epoch""" + + type: int diff --git a/tests/api_resources/test_contexts.py b/tests/api_resources/test_contexts.py index 4ad27733..31fb97d0 100644 --- a/tests/api_resources/test_contexts.py +++ b/tests/api_resources/test_contexts.py @@ -9,11 +9,7 @@ from browserbase import Browserbase, AsyncBrowserbase from tests.utils import assert_matches_type -from browserbase.types import ( - ContextCreateResponse, - ContextUpdateResponse, - ContextRetrieveResponse, -) +from browserbase.types import Context, ContextCreateResponse, ContextUpdateResponse base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") @@ -23,6 +19,11 @@ class TestContexts: @parametrize def test_method_create(self, client: Browserbase) -> None: + context = client.contexts.create() + assert_matches_type(ContextCreateResponse, context, path=["response"]) + + @parametrize + def test_method_create_with_all_params(self, client: Browserbase) -> None: context = client.contexts.create( project_id="projectId", ) @@ -30,9 +31,7 @@ def test_method_create(self, client: Browserbase) -> None: @parametrize def test_raw_response_create(self, client: Browserbase) -> None: - response = client.contexts.with_raw_response.create( - project_id="projectId", - ) + response = client.contexts.with_raw_response.create() assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -41,9 +40,7 @@ def test_raw_response_create(self, client: Browserbase) -> None: @parametrize def test_streaming_response_create(self, client: Browserbase) -> None: - with client.contexts.with_streaming_response.create( - project_id="projectId", - ) as response: + with client.contexts.with_streaming_response.create() as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -57,7 +54,7 @@ def test_method_retrieve(self, client: Browserbase) -> None: context = client.contexts.retrieve( "id", ) - assert_matches_type(ContextRetrieveResponse, context, path=["response"]) + assert_matches_type(Context, context, path=["response"]) @parametrize def test_raw_response_retrieve(self, client: Browserbase) -> None: @@ -68,7 +65,7 @@ def test_raw_response_retrieve(self, client: Browserbase) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" context = response.parse() - assert_matches_type(ContextRetrieveResponse, context, path=["response"]) + assert_matches_type(Context, context, path=["response"]) @parametrize def test_streaming_response_retrieve(self, client: Browserbase) -> None: @@ -79,7 +76,7 @@ def test_streaming_response_retrieve(self, client: Browserbase) -> None: assert response.http_request.headers.get("X-Stainless-Lang") == "python" context = response.parse() - assert_matches_type(ContextRetrieveResponse, context, path=["response"]) + assert_matches_type(Context, context, path=["response"]) assert cast(Any, response.is_closed) is True @@ -128,6 +125,44 @@ def test_path_params_update(self, client: Browserbase) -> None: "", ) + @parametrize + def test_method_delete(self, client: Browserbase) -> None: + context = client.contexts.delete( + "id", + ) + assert context is None + + @parametrize + def test_raw_response_delete(self, client: Browserbase) -> None: + response = client.contexts.with_raw_response.delete( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + context = response.parse() + assert context is None + + @parametrize + def test_streaming_response_delete(self, client: Browserbase) -> None: + with client.contexts.with_streaming_response.delete( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + context = response.parse() + assert context is None + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_delete(self, client: Browserbase) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.contexts.with_raw_response.delete( + "", + ) + class TestAsyncContexts: parametrize = pytest.mark.parametrize( @@ -136,6 +171,11 @@ class TestAsyncContexts: @parametrize async def test_method_create(self, async_client: AsyncBrowserbase) -> None: + context = await async_client.contexts.create() + assert_matches_type(ContextCreateResponse, context, path=["response"]) + + @parametrize + async def test_method_create_with_all_params(self, async_client: AsyncBrowserbase) -> None: context = await async_client.contexts.create( project_id="projectId", ) @@ -143,9 +183,7 @@ async def test_method_create(self, async_client: AsyncBrowserbase) -> None: @parametrize async def test_raw_response_create(self, async_client: AsyncBrowserbase) -> None: - response = await async_client.contexts.with_raw_response.create( - project_id="projectId", - ) + response = await async_client.contexts.with_raw_response.create() assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -154,9 +192,7 @@ async def test_raw_response_create(self, async_client: AsyncBrowserbase) -> None @parametrize async def test_streaming_response_create(self, async_client: AsyncBrowserbase) -> None: - async with async_client.contexts.with_streaming_response.create( - project_id="projectId", - ) as response: + async with async_client.contexts.with_streaming_response.create() as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -170,7 +206,7 @@ async def test_method_retrieve(self, async_client: AsyncBrowserbase) -> None: context = await async_client.contexts.retrieve( "id", ) - assert_matches_type(ContextRetrieveResponse, context, path=["response"]) + assert_matches_type(Context, context, path=["response"]) @parametrize async def test_raw_response_retrieve(self, async_client: AsyncBrowserbase) -> None: @@ -181,7 +217,7 @@ async def test_raw_response_retrieve(self, async_client: AsyncBrowserbase) -> No assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" context = await response.parse() - assert_matches_type(ContextRetrieveResponse, context, path=["response"]) + assert_matches_type(Context, context, path=["response"]) @parametrize async def test_streaming_response_retrieve(self, async_client: AsyncBrowserbase) -> None: @@ -192,7 +228,7 @@ async def test_streaming_response_retrieve(self, async_client: AsyncBrowserbase) assert response.http_request.headers.get("X-Stainless-Lang") == "python" context = await response.parse() - assert_matches_type(ContextRetrieveResponse, context, path=["response"]) + assert_matches_type(Context, context, path=["response"]) assert cast(Any, response.is_closed) is True @@ -240,3 +276,41 @@ async def test_path_params_update(self, async_client: AsyncBrowserbase) -> None: await async_client.contexts.with_raw_response.update( "", ) + + @parametrize + async def test_method_delete(self, async_client: AsyncBrowserbase) -> None: + context = await async_client.contexts.delete( + "id", + ) + assert context is None + + @parametrize + async def test_raw_response_delete(self, async_client: AsyncBrowserbase) -> None: + response = await async_client.contexts.with_raw_response.delete( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + context = await response.parse() + assert context is None + + @parametrize + async def test_streaming_response_delete(self, async_client: AsyncBrowserbase) -> None: + async with async_client.contexts.with_streaming_response.delete( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + context = await response.parse() + assert context is None + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_delete(self, async_client: AsyncBrowserbase) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.contexts.with_raw_response.delete( + "", + ) diff --git a/tests/api_resources/test_extensions.py b/tests/api_resources/test_extensions.py index e32ae9b0..6b6a0183 100644 --- a/tests/api_resources/test_extensions.py +++ b/tests/api_resources/test_extensions.py @@ -9,7 +9,7 @@ from browserbase import Browserbase, AsyncBrowserbase from tests.utils import assert_matches_type -from browserbase.types import ExtensionCreateResponse, ExtensionRetrieveResponse +from browserbase.types import Extension base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") @@ -22,7 +22,7 @@ def test_method_create(self, client: Browserbase) -> None: extension = client.extensions.create( file=b"raw file contents", ) - assert_matches_type(ExtensionCreateResponse, extension, path=["response"]) + assert_matches_type(Extension, extension, path=["response"]) @parametrize def test_raw_response_create(self, client: Browserbase) -> None: @@ -33,7 +33,7 @@ def test_raw_response_create(self, client: Browserbase) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" extension = response.parse() - assert_matches_type(ExtensionCreateResponse, extension, path=["response"]) + assert_matches_type(Extension, extension, path=["response"]) @parametrize def test_streaming_response_create(self, client: Browserbase) -> None: @@ -44,7 +44,7 @@ def test_streaming_response_create(self, client: Browserbase) -> None: assert response.http_request.headers.get("X-Stainless-Lang") == "python" extension = response.parse() - assert_matches_type(ExtensionCreateResponse, extension, path=["response"]) + assert_matches_type(Extension, extension, path=["response"]) assert cast(Any, response.is_closed) is True @@ -53,7 +53,7 @@ def test_method_retrieve(self, client: Browserbase) -> None: extension = client.extensions.retrieve( "id", ) - assert_matches_type(ExtensionRetrieveResponse, extension, path=["response"]) + assert_matches_type(Extension, extension, path=["response"]) @parametrize def test_raw_response_retrieve(self, client: Browserbase) -> None: @@ -64,7 +64,7 @@ def test_raw_response_retrieve(self, client: Browserbase) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" extension = response.parse() - assert_matches_type(ExtensionRetrieveResponse, extension, path=["response"]) + assert_matches_type(Extension, extension, path=["response"]) @parametrize def test_streaming_response_retrieve(self, client: Browserbase) -> None: @@ -75,7 +75,7 @@ def test_streaming_response_retrieve(self, client: Browserbase) -> None: assert response.http_request.headers.get("X-Stainless-Lang") == "python" extension = response.parse() - assert_matches_type(ExtensionRetrieveResponse, extension, path=["response"]) + assert_matches_type(Extension, extension, path=["response"]) assert cast(Any, response.is_closed) is True @@ -135,7 +135,7 @@ async def test_method_create(self, async_client: AsyncBrowserbase) -> None: extension = await async_client.extensions.create( file=b"raw file contents", ) - assert_matches_type(ExtensionCreateResponse, extension, path=["response"]) + assert_matches_type(Extension, extension, path=["response"]) @parametrize async def test_raw_response_create(self, async_client: AsyncBrowserbase) -> None: @@ -146,7 +146,7 @@ async def test_raw_response_create(self, async_client: AsyncBrowserbase) -> None assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" extension = await response.parse() - assert_matches_type(ExtensionCreateResponse, extension, path=["response"]) + assert_matches_type(Extension, extension, path=["response"]) @parametrize async def test_streaming_response_create(self, async_client: AsyncBrowserbase) -> None: @@ -157,7 +157,7 @@ async def test_streaming_response_create(self, async_client: AsyncBrowserbase) - assert response.http_request.headers.get("X-Stainless-Lang") == "python" extension = await response.parse() - assert_matches_type(ExtensionCreateResponse, extension, path=["response"]) + assert_matches_type(Extension, extension, path=["response"]) assert cast(Any, response.is_closed) is True @@ -166,7 +166,7 @@ async def test_method_retrieve(self, async_client: AsyncBrowserbase) -> None: extension = await async_client.extensions.retrieve( "id", ) - assert_matches_type(ExtensionRetrieveResponse, extension, path=["response"]) + assert_matches_type(Extension, extension, path=["response"]) @parametrize async def test_raw_response_retrieve(self, async_client: AsyncBrowserbase) -> None: @@ -177,7 +177,7 @@ async def test_raw_response_retrieve(self, async_client: AsyncBrowserbase) -> No assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" extension = await response.parse() - assert_matches_type(ExtensionRetrieveResponse, extension, path=["response"]) + assert_matches_type(Extension, extension, path=["response"]) @parametrize async def test_streaming_response_retrieve(self, async_client: AsyncBrowserbase) -> None: @@ -188,7 +188,7 @@ async def test_streaming_response_retrieve(self, async_client: AsyncBrowserbase) assert response.http_request.headers.get("X-Stainless-Lang") == "python" extension = await response.parse() - assert_matches_type(ExtensionRetrieveResponse, extension, path=["response"]) + assert_matches_type(Extension, extension, path=["response"]) assert cast(Any, response.is_closed) is True diff --git a/tests/api_resources/test_projects.py b/tests/api_resources/test_projects.py index 0d8e3c94..c8241bf8 100644 --- a/tests/api_resources/test_projects.py +++ b/tests/api_resources/test_projects.py @@ -9,7 +9,7 @@ from browserbase import Browserbase, AsyncBrowserbase from tests.utils import assert_matches_type -from browserbase.types import ProjectListResponse, ProjectUsageResponse, ProjectRetrieveResponse +from browserbase.types import Project, ProjectUsage, ProjectListResponse base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") @@ -22,7 +22,7 @@ def test_method_retrieve(self, client: Browserbase) -> None: project = client.projects.retrieve( "id", ) - assert_matches_type(ProjectRetrieveResponse, project, path=["response"]) + assert_matches_type(Project, project, path=["response"]) @parametrize def test_raw_response_retrieve(self, client: Browserbase) -> None: @@ -33,7 +33,7 @@ def test_raw_response_retrieve(self, client: Browserbase) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" project = response.parse() - assert_matches_type(ProjectRetrieveResponse, project, path=["response"]) + assert_matches_type(Project, project, path=["response"]) @parametrize def test_streaming_response_retrieve(self, client: Browserbase) -> None: @@ -44,7 +44,7 @@ def test_streaming_response_retrieve(self, client: Browserbase) -> None: assert response.http_request.headers.get("X-Stainless-Lang") == "python" project = response.parse() - assert_matches_type(ProjectRetrieveResponse, project, path=["response"]) + assert_matches_type(Project, project, path=["response"]) assert cast(Any, response.is_closed) is True @@ -85,7 +85,7 @@ def test_method_usage(self, client: Browserbase) -> None: project = client.projects.usage( "id", ) - assert_matches_type(ProjectUsageResponse, project, path=["response"]) + assert_matches_type(ProjectUsage, project, path=["response"]) @parametrize def test_raw_response_usage(self, client: Browserbase) -> None: @@ -96,7 +96,7 @@ def test_raw_response_usage(self, client: Browserbase) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" project = response.parse() - assert_matches_type(ProjectUsageResponse, project, path=["response"]) + assert_matches_type(ProjectUsage, project, path=["response"]) @parametrize def test_streaming_response_usage(self, client: Browserbase) -> None: @@ -107,7 +107,7 @@ def test_streaming_response_usage(self, client: Browserbase) -> None: assert response.http_request.headers.get("X-Stainless-Lang") == "python" project = response.parse() - assert_matches_type(ProjectUsageResponse, project, path=["response"]) + assert_matches_type(ProjectUsage, project, path=["response"]) assert cast(Any, response.is_closed) is True @@ -129,7 +129,7 @@ async def test_method_retrieve(self, async_client: AsyncBrowserbase) -> None: project = await async_client.projects.retrieve( "id", ) - assert_matches_type(ProjectRetrieveResponse, project, path=["response"]) + assert_matches_type(Project, project, path=["response"]) @parametrize async def test_raw_response_retrieve(self, async_client: AsyncBrowserbase) -> None: @@ -140,7 +140,7 @@ async def test_raw_response_retrieve(self, async_client: AsyncBrowserbase) -> No assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" project = await response.parse() - assert_matches_type(ProjectRetrieveResponse, project, path=["response"]) + assert_matches_type(Project, project, path=["response"]) @parametrize async def test_streaming_response_retrieve(self, async_client: AsyncBrowserbase) -> None: @@ -151,7 +151,7 @@ async def test_streaming_response_retrieve(self, async_client: AsyncBrowserbase) assert response.http_request.headers.get("X-Stainless-Lang") == "python" project = await response.parse() - assert_matches_type(ProjectRetrieveResponse, project, path=["response"]) + assert_matches_type(Project, project, path=["response"]) assert cast(Any, response.is_closed) is True @@ -192,7 +192,7 @@ async def test_method_usage(self, async_client: AsyncBrowserbase) -> None: project = await async_client.projects.usage( "id", ) - assert_matches_type(ProjectUsageResponse, project, path=["response"]) + assert_matches_type(ProjectUsage, project, path=["response"]) @parametrize async def test_raw_response_usage(self, async_client: AsyncBrowserbase) -> None: @@ -203,7 +203,7 @@ async def test_raw_response_usage(self, async_client: AsyncBrowserbase) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" project = await response.parse() - assert_matches_type(ProjectUsageResponse, project, path=["response"]) + assert_matches_type(ProjectUsage, project, path=["response"]) @parametrize async def test_streaming_response_usage(self, async_client: AsyncBrowserbase) -> None: @@ -214,7 +214,7 @@ async def test_streaming_response_usage(self, async_client: AsyncBrowserbase) -> assert response.http_request.headers.get("X-Stainless-Lang") == "python" project = await response.parse() - assert_matches_type(ProjectUsageResponse, project, path=["response"]) + assert_matches_type(ProjectUsage, project, path=["response"]) assert cast(Any, response.is_closed) is True diff --git a/tests/api_resources/test_sessions.py b/tests/api_resources/test_sessions.py index 7a16f64f..6a21aa85 100644 --- a/tests/api_resources/test_sessions.py +++ b/tests/api_resources/test_sessions.py @@ -10,10 +10,10 @@ from browserbase import Browserbase, AsyncBrowserbase from tests.utils import assert_matches_type from browserbase.types import ( + Session, + SessionLiveURLs, SessionListResponse, - SessionDebugResponse, SessionCreateResponse, - SessionUpdateResponse, SessionRetrieveResponse, ) @@ -25,15 +25,12 @@ class TestSessions: @parametrize def test_method_create(self, client: Browserbase) -> None: - session = client.sessions.create( - project_id="projectId", - ) + session = client.sessions.create() assert_matches_type(SessionCreateResponse, session, path=["response"]) @parametrize def test_method_create_with_all_params(self, client: Browserbase) -> None: session = client.sessions.create( - project_id="projectId", browser_settings={ "advanced_stealth": True, "block_ads": True, @@ -44,19 +41,6 @@ def test_method_create_with_all_params(self, client: Browserbase) -> None: "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, "os": "windows", "record_session": True, @@ -68,17 +52,8 @@ def test_method_create_with_all_params(self, client: Browserbase) -> None: }, extension_id="extensionId", keep_alive=True, - proxies=[ - { - "type": "browserbase", - "domain_pattern": "domainPattern", - "geolocation": { - "country": "xx", - "city": "city", - "state": "xx", - }, - } - ], + project_id="projectId", + proxies=True, region="us-west-2", api_timeout=60, user_metadata={"foo": "bar"}, @@ -87,9 +62,7 @@ def test_method_create_with_all_params(self, client: Browserbase) -> None: @parametrize def test_raw_response_create(self, client: Browserbase) -> None: - response = client.sessions.with_raw_response.create( - project_id="projectId", - ) + response = client.sessions.with_raw_response.create() assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -98,9 +71,7 @@ def test_raw_response_create(self, client: Browserbase) -> None: @parametrize def test_streaming_response_create(self, client: Browserbase) -> None: - with client.sessions.with_streaming_response.create( - project_id="projectId", - ) as response: + with client.sessions.with_streaming_response.create() as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -151,36 +122,42 @@ def test_path_params_retrieve(self, client: Browserbase) -> None: def test_method_update(self, client: Browserbase) -> None: session = client.sessions.update( id="id", - project_id="projectId", status="REQUEST_RELEASE", ) - assert_matches_type(SessionUpdateResponse, session, path=["response"]) + assert_matches_type(Session, session, path=["response"]) + + @parametrize + def test_method_update_with_all_params(self, client: Browserbase) -> None: + session = client.sessions.update( + id="id", + status="REQUEST_RELEASE", + project_id="projectId", + ) + assert_matches_type(Session, session, path=["response"]) @parametrize def test_raw_response_update(self, client: Browserbase) -> None: response = client.sessions.with_raw_response.update( id="id", - project_id="projectId", status="REQUEST_RELEASE", ) assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" session = response.parse() - assert_matches_type(SessionUpdateResponse, session, path=["response"]) + assert_matches_type(Session, session, path=["response"]) @parametrize def test_streaming_response_update(self, client: Browserbase) -> None: with client.sessions.with_streaming_response.update( id="id", - project_id="projectId", status="REQUEST_RELEASE", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" session = response.parse() - assert_matches_type(SessionUpdateResponse, session, path=["response"]) + assert_matches_type(Session, session, path=["response"]) assert cast(Any, response.is_closed) is True @@ -189,7 +166,6 @@ def test_path_params_update(self, client: Browserbase) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): client.sessions.with_raw_response.update( id="", - project_id="projectId", status="REQUEST_RELEASE", ) @@ -231,7 +207,7 @@ def test_method_debug(self, client: Browserbase) -> None: session = client.sessions.debug( "id", ) - assert_matches_type(SessionDebugResponse, session, path=["response"]) + assert_matches_type(SessionLiveURLs, session, path=["response"]) @parametrize def test_raw_response_debug(self, client: Browserbase) -> None: @@ -242,7 +218,7 @@ def test_raw_response_debug(self, client: Browserbase) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" session = response.parse() - assert_matches_type(SessionDebugResponse, session, path=["response"]) + assert_matches_type(SessionLiveURLs, session, path=["response"]) @parametrize def test_streaming_response_debug(self, client: Browserbase) -> None: @@ -253,7 +229,7 @@ def test_streaming_response_debug(self, client: Browserbase) -> None: assert response.http_request.headers.get("X-Stainless-Lang") == "python" session = response.parse() - assert_matches_type(SessionDebugResponse, session, path=["response"]) + assert_matches_type(SessionLiveURLs, session, path=["response"]) assert cast(Any, response.is_closed) is True @@ -272,15 +248,12 @@ class TestAsyncSessions: @parametrize async def test_method_create(self, async_client: AsyncBrowserbase) -> None: - session = await async_client.sessions.create( - project_id="projectId", - ) + session = await async_client.sessions.create() assert_matches_type(SessionCreateResponse, session, path=["response"]) @parametrize async def test_method_create_with_all_params(self, async_client: AsyncBrowserbase) -> None: session = await async_client.sessions.create( - project_id="projectId", browser_settings={ "advanced_stealth": True, "block_ads": True, @@ -291,19 +264,6 @@ async def test_method_create_with_all_params(self, async_client: AsyncBrowserbas "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, "os": "windows", "record_session": True, @@ -315,17 +275,8 @@ async def test_method_create_with_all_params(self, async_client: AsyncBrowserbas }, extension_id="extensionId", keep_alive=True, - proxies=[ - { - "type": "browserbase", - "domain_pattern": "domainPattern", - "geolocation": { - "country": "xx", - "city": "city", - "state": "xx", - }, - } - ], + project_id="projectId", + proxies=True, region="us-west-2", api_timeout=60, user_metadata={"foo": "bar"}, @@ -334,9 +285,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncBrowserbas @parametrize async def test_raw_response_create(self, async_client: AsyncBrowserbase) -> None: - response = await async_client.sessions.with_raw_response.create( - project_id="projectId", - ) + response = await async_client.sessions.with_raw_response.create() assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -345,9 +294,7 @@ async def test_raw_response_create(self, async_client: AsyncBrowserbase) -> None @parametrize async def test_streaming_response_create(self, async_client: AsyncBrowserbase) -> None: - async with async_client.sessions.with_streaming_response.create( - project_id="projectId", - ) as response: + async with async_client.sessions.with_streaming_response.create() as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -398,36 +345,42 @@ async def test_path_params_retrieve(self, async_client: AsyncBrowserbase) -> Non async def test_method_update(self, async_client: AsyncBrowserbase) -> None: session = await async_client.sessions.update( id="id", - project_id="projectId", status="REQUEST_RELEASE", ) - assert_matches_type(SessionUpdateResponse, session, path=["response"]) + assert_matches_type(Session, session, path=["response"]) + + @parametrize + async def test_method_update_with_all_params(self, async_client: AsyncBrowserbase) -> None: + session = await async_client.sessions.update( + id="id", + status="REQUEST_RELEASE", + project_id="projectId", + ) + assert_matches_type(Session, session, path=["response"]) @parametrize async def test_raw_response_update(self, async_client: AsyncBrowserbase) -> None: response = await async_client.sessions.with_raw_response.update( id="id", - project_id="projectId", status="REQUEST_RELEASE", ) assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" session = await response.parse() - assert_matches_type(SessionUpdateResponse, session, path=["response"]) + assert_matches_type(Session, session, path=["response"]) @parametrize async def test_streaming_response_update(self, async_client: AsyncBrowserbase) -> None: async with async_client.sessions.with_streaming_response.update( id="id", - project_id="projectId", status="REQUEST_RELEASE", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" session = await response.parse() - assert_matches_type(SessionUpdateResponse, session, path=["response"]) + assert_matches_type(Session, session, path=["response"]) assert cast(Any, response.is_closed) is True @@ -436,7 +389,6 @@ async def test_path_params_update(self, async_client: AsyncBrowserbase) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): await async_client.sessions.with_raw_response.update( id="", - project_id="projectId", status="REQUEST_RELEASE", ) @@ -478,7 +430,7 @@ async def test_method_debug(self, async_client: AsyncBrowserbase) -> None: session = await async_client.sessions.debug( "id", ) - assert_matches_type(SessionDebugResponse, session, path=["response"]) + assert_matches_type(SessionLiveURLs, session, path=["response"]) @parametrize async def test_raw_response_debug(self, async_client: AsyncBrowserbase) -> None: @@ -489,7 +441,7 @@ async def test_raw_response_debug(self, async_client: AsyncBrowserbase) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" session = await response.parse() - assert_matches_type(SessionDebugResponse, session, path=["response"]) + assert_matches_type(SessionLiveURLs, session, path=["response"]) @parametrize async def test_streaming_response_debug(self, async_client: AsyncBrowserbase) -> None: @@ -500,7 +452,7 @@ async def test_streaming_response_debug(self, async_client: AsyncBrowserbase) -> assert response.http_request.headers.get("X-Stainless-Lang") == "python" session = await response.parse() - assert_matches_type(SessionDebugResponse, session, path=["response"]) + assert_matches_type(SessionLiveURLs, session, path=["response"]) assert cast(Any, response.is_closed) is True diff --git a/tests/test_client.py b/tests/test_client.py index 608cc1f9..71396d22 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -864,7 +864,7 @@ def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter, clien respx_mock.post("/v1/sessions").mock(side_effect=httpx.TimeoutException("Test timeout error")) with pytest.raises(APITimeoutError): - client.sessions.with_streaming_response.create(project_id="projectId").__enter__() + client.sessions.with_streaming_response.create().__enter__() assert _get_open_connections(client) == 0 @@ -874,7 +874,7 @@ def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter, client respx_mock.post("/v1/sessions").mock(return_value=httpx.Response(500)) with pytest.raises(APIStatusError): - client.sessions.with_streaming_response.create(project_id="projectId").__enter__() + client.sessions.with_streaming_response.create().__enter__() assert _get_open_connections(client) == 0 @pytest.mark.parametrize("failures_before_success", [0, 2, 4]) @@ -903,7 +903,7 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: respx_mock.post("/v1/sessions").mock(side_effect=retry_handler) - response = client.sessions.with_raw_response.create(project_id="projectId") + response = client.sessions.with_raw_response.create() assert response.retries_taken == failures_before_success assert int(response.http_request.headers.get("x-stainless-retry-count")) == failures_before_success @@ -927,9 +927,7 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: respx_mock.post("/v1/sessions").mock(side_effect=retry_handler) - response = client.sessions.with_raw_response.create( - project_id="projectId", extra_headers={"x-stainless-retry-count": Omit()} - ) + response = client.sessions.with_raw_response.create(extra_headers={"x-stainless-retry-count": Omit()}) assert len(response.http_request.headers.get_list("x-stainless-retry-count")) == 0 @@ -952,9 +950,7 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: respx_mock.post("/v1/sessions").mock(side_effect=retry_handler) - response = client.sessions.with_raw_response.create( - project_id="projectId", extra_headers={"x-stainless-retry-count": "42"} - ) + response = client.sessions.with_raw_response.create(extra_headers={"x-stainless-retry-count": "42"}) assert response.http_request.headers.get("x-stainless-retry-count") == "42" @@ -1770,7 +1766,7 @@ async def test_retrying_timeout_errors_doesnt_leak( respx_mock.post("/v1/sessions").mock(side_effect=httpx.TimeoutException("Test timeout error")) with pytest.raises(APITimeoutError): - await async_client.sessions.with_streaming_response.create(project_id="projectId").__aenter__() + await async_client.sessions.with_streaming_response.create().__aenter__() assert _get_open_connections(async_client) == 0 @@ -1782,7 +1778,7 @@ async def test_retrying_status_errors_doesnt_leak( respx_mock.post("/v1/sessions").mock(return_value=httpx.Response(500)) with pytest.raises(APIStatusError): - await async_client.sessions.with_streaming_response.create(project_id="projectId").__aenter__() + await async_client.sessions.with_streaming_response.create().__aenter__() assert _get_open_connections(async_client) == 0 @pytest.mark.parametrize("failures_before_success", [0, 2, 4]) @@ -1811,7 +1807,7 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: respx_mock.post("/v1/sessions").mock(side_effect=retry_handler) - response = await client.sessions.with_raw_response.create(project_id="projectId") + response = await client.sessions.with_raw_response.create() assert response.retries_taken == failures_before_success assert int(response.http_request.headers.get("x-stainless-retry-count")) == failures_before_success @@ -1835,9 +1831,7 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: respx_mock.post("/v1/sessions").mock(side_effect=retry_handler) - response = await client.sessions.with_raw_response.create( - project_id="projectId", extra_headers={"x-stainless-retry-count": Omit()} - ) + response = await client.sessions.with_raw_response.create(extra_headers={"x-stainless-retry-count": Omit()}) assert len(response.http_request.headers.get_list("x-stainless-retry-count")) == 0 @@ -1860,9 +1854,7 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: respx_mock.post("/v1/sessions").mock(side_effect=retry_handler) - response = await client.sessions.with_raw_response.create( - project_id="projectId", extra_headers={"x-stainless-retry-count": "42"} - ) + response = await client.sessions.with_raw_response.create(extra_headers={"x-stainless-retry-count": "42"}) assert response.http_request.headers.get("x-stainless-retry-count") == "42" From a9ababf4f54b88924f1f1a4c5633ba373792c5fa Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 23 Feb 2026 23:05:21 +0000 Subject: [PATCH 243/330] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index 9a94e549..834ae8a7 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 19 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fbrowserbase-b92143ddb16135de4ff65ce8bcdfe9991d11c73570f42f07ea27e0da86209a44.yml openapi_spec_hash: 16eb6e6c9687f01d2a791775b27dc315 -config_hash: b01d72cbe03bd762a73b05744086b2ec +config_hash: 99e5318dc8a8d75839ec565d94276c71 From 48afa4d333780008dd10d6f48de1e4c987e66bdf Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 23 Feb 2026 23:06:25 +0000 Subject: [PATCH 244/330] feat(api): manual updates --- .stats.yml | 4 +-- .../resources/sessions/sessions.py | 30 +++++++++---------- .../types/session_create_params.py | 12 ++++---- tests/api_resources/test_sessions.py | 4 +-- 4 files changed, 24 insertions(+), 26 deletions(-) diff --git a/.stats.yml b/.stats.yml index 834ae8a7..2d76bb2b 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 19 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fbrowserbase-b92143ddb16135de4ff65ce8bcdfe9991d11c73570f42f07ea27e0da86209a44.yml -openapi_spec_hash: 16eb6e6c9687f01d2a791775b27dc315 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fbrowserbase-af59f86ea3ee682d54560eaf6055df5bebacee560749f5f3cbd9c8ff8b5fa8e5.yml +openapi_spec_hash: 72dfdba2efc46c7cbbaa98bd234b72de config_hash: 99e5318dc8a8d75839ec565d94276c71 diff --git a/src/browserbase/resources/sessions/sessions.py b/src/browserbase/resources/sessions/sessions.py index 09fd15a3..49ab9ce6 100644 --- a/src/browserbase/resources/sessions/sessions.py +++ b/src/browserbase/resources/sessions/sessions.py @@ -99,13 +99,13 @@ def with_streaming_response(self) -> SessionsResourceWithStreamingResponse: def create( self, *, + api_timeout: int | Omit = omit, browser_settings: session_create_params.BrowserSettings | Omit = omit, extension_id: str | Omit = omit, keep_alive: bool | Omit = omit, project_id: str | Omit = omit, proxies: Union[bool, Iterable[session_create_params.ProxiesUnionMember1]] | Omit = omit, region: Literal["us-west-2", "us-east-1", "eu-central-1", "ap-southeast-1"] | Omit = omit, - api_timeout: int | Omit = omit, user_metadata: Dict[str, 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. @@ -114,12 +114,14 @@ def create( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> SessionCreateResponse: - """Create a Session + """ + Create a Session Args: - extension_id: The uploaded Extension ID. + api_timeout: Duration in seconds after which the session will automatically end. Defaults to + the Project's `defaultTimeout`. - See + extension_id: The uploaded Extension ID. See [Upload Extension](/reference/api/upload-an-extension). keep_alive: Set to true to keep the session alive even after disconnections. Available on @@ -133,9 +135,6 @@ def create( region: The region where the Session should run. - api_timeout: Duration in seconds after which the session will automatically end. Defaults to - the Project's `defaultTimeout`. - user_metadata: Arbitrary user metadata to attach to the session. To learn more about user metadata, see [User Metadata](/features/sessions#user-metadata). @@ -151,13 +150,13 @@ def create( "/v1/sessions", body=maybe_transform( { + "api_timeout": api_timeout, "browser_settings": browser_settings, "extension_id": extension_id, "keep_alive": keep_alive, "project_id": project_id, "proxies": proxies, "region": region, - "api_timeout": api_timeout, "user_metadata": user_metadata, }, session_create_params.SessionCreateParams, @@ -369,13 +368,13 @@ def with_streaming_response(self) -> AsyncSessionsResourceWithStreamingResponse: async def create( self, *, + api_timeout: int | Omit = omit, browser_settings: session_create_params.BrowserSettings | Omit = omit, extension_id: str | Omit = omit, keep_alive: bool | Omit = omit, project_id: str | Omit = omit, proxies: Union[bool, Iterable[session_create_params.ProxiesUnionMember1]] | Omit = omit, region: Literal["us-west-2", "us-east-1", "eu-central-1", "ap-southeast-1"] | Omit = omit, - api_timeout: int | Omit = omit, user_metadata: Dict[str, 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. @@ -384,12 +383,14 @@ async def create( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> SessionCreateResponse: - """Create a Session + """ + Create a Session Args: - extension_id: The uploaded Extension ID. + api_timeout: Duration in seconds after which the session will automatically end. Defaults to + the Project's `defaultTimeout`. - See + extension_id: The uploaded Extension ID. See [Upload Extension](/reference/api/upload-an-extension). keep_alive: Set to true to keep the session alive even after disconnections. Available on @@ -403,9 +404,6 @@ async def create( region: The region where the Session should run. - api_timeout: Duration in seconds after which the session will automatically end. Defaults to - the Project's `defaultTimeout`. - user_metadata: Arbitrary user metadata to attach to the session. To learn more about user metadata, see [User Metadata](/features/sessions#user-metadata). @@ -421,13 +419,13 @@ async def create( "/v1/sessions", body=await async_maybe_transform( { + "api_timeout": api_timeout, "browser_settings": browser_settings, "extension_id": extension_id, "keep_alive": keep_alive, "project_id": project_id, "proxies": proxies, "region": region, - "api_timeout": api_timeout, "user_metadata": user_metadata, }, session_create_params.SessionCreateParams, diff --git a/src/browserbase/types/session_create_params.py b/src/browserbase/types/session_create_params.py index 33dd6fd5..24d71f0f 100644 --- a/src/browserbase/types/session_create_params.py +++ b/src/browserbase/types/session_create_params.py @@ -21,6 +21,12 @@ class SessionCreateParams(TypedDict, total=False): + api_timeout: int + """Duration in seconds after which the session will automatically end. + + Defaults to the Project's `defaultTimeout`. + """ + browser_settings: Annotated[BrowserSettings, PropertyInfo(alias="browserSettings")] extension_id: Annotated[str, PropertyInfo(alias="extensionId")] @@ -50,12 +56,6 @@ class SessionCreateParams(TypedDict, total=False): region: Literal["us-west-2", "us-east-1", "eu-central-1", "ap-southeast-1"] """The region where the Session should run.""" - api_timeout: Annotated[int, PropertyInfo(alias="timeout")] - """Duration in seconds after which the session will automatically end. - - Defaults to the Project's `defaultTimeout`. - """ - user_metadata: Annotated[Dict[str, object], PropertyInfo(alias="userMetadata")] """Arbitrary user metadata to attach to the session. diff --git a/tests/api_resources/test_sessions.py b/tests/api_resources/test_sessions.py index 6a21aa85..571daefd 100644 --- a/tests/api_resources/test_sessions.py +++ b/tests/api_resources/test_sessions.py @@ -31,6 +31,7 @@ def test_method_create(self, client: Browserbase) -> None: @parametrize def test_method_create_with_all_params(self, client: Browserbase) -> None: session = client.sessions.create( + api_timeout=60, browser_settings={ "advanced_stealth": True, "block_ads": True, @@ -55,7 +56,6 @@ def test_method_create_with_all_params(self, client: Browserbase) -> None: project_id="projectId", proxies=True, region="us-west-2", - api_timeout=60, user_metadata={"foo": "bar"}, ) assert_matches_type(SessionCreateResponse, session, path=["response"]) @@ -254,6 +254,7 @@ async def test_method_create(self, async_client: AsyncBrowserbase) -> None: @parametrize async def test_method_create_with_all_params(self, async_client: AsyncBrowserbase) -> None: session = await async_client.sessions.create( + api_timeout=60, browser_settings={ "advanced_stealth": True, "block_ads": True, @@ -278,7 +279,6 @@ async def test_method_create_with_all_params(self, async_client: AsyncBrowserbas project_id="projectId", proxies=True, region="us-west-2", - api_timeout=60, user_metadata={"foo": "bar"}, ) assert_matches_type(SessionCreateResponse, session, path=["response"]) From 365be274fb159254c4c0f645b3238894adb560e3 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 23 Feb 2026 23:16:02 +0000 Subject: [PATCH 245/330] feat(api): manual updates --- .stats.yml | 4 +- .../resources/sessions/sessions.py | 6 +- .../types/session_create_params.py | 83 +------------------ 3 files changed, 9 insertions(+), 84 deletions(-) diff --git a/.stats.yml b/.stats.yml index 2d76bb2b..05a7ecdb 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 19 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fbrowserbase-af59f86ea3ee682d54560eaf6055df5bebacee560749f5f3cbd9c8ff8b5fa8e5.yml +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fbrowserbase-a9cd420dd3e871aaaad18fb9def41d277ae000607f369657c965a30e779c221a.yml openapi_spec_hash: 72dfdba2efc46c7cbbaa98bd234b72de -config_hash: 99e5318dc8a8d75839ec565d94276c71 +config_hash: b32e9f369edd843015c9b9d4fbc3b4f9 diff --git a/src/browserbase/resources/sessions/sessions.py b/src/browserbase/resources/sessions/sessions.py index 49ab9ce6..e7523a8c 100644 --- a/src/browserbase/resources/sessions/sessions.py +++ b/src/browserbase/resources/sessions/sessions.py @@ -2,7 +2,7 @@ from __future__ import annotations -from typing import Dict, Union, Iterable +from typing import Dict, Union from typing_extensions import Literal import httpx @@ -104,7 +104,7 @@ def create( extension_id: str | Omit = omit, keep_alive: bool | Omit = omit, project_id: str | Omit = omit, - proxies: Union[bool, Iterable[session_create_params.ProxiesUnionMember1]] | Omit = omit, + proxies: Union[bool, object] | Omit = omit, region: Literal["us-west-2", "us-east-1", "eu-central-1", "ap-southeast-1"] | Omit = omit, user_metadata: Dict[str, object] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. @@ -373,7 +373,7 @@ async def create( extension_id: str | Omit = omit, keep_alive: bool | Omit = omit, project_id: str | Omit = omit, - proxies: Union[bool, Iterable[session_create_params.ProxiesUnionMember1]] | Omit = omit, + proxies: Union[bool, object] | Omit = omit, region: Literal["us-west-2", "us-east-1", "eu-central-1", "ap-southeast-1"] | Omit = omit, user_metadata: Dict[str, object] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. diff --git a/src/browserbase/types/session_create_params.py b/src/browserbase/types/session_create_params.py index 24d71f0f..dc495448 100644 --- a/src/browserbase/types/session_create_params.py +++ b/src/browserbase/types/session_create_params.py @@ -2,22 +2,12 @@ from __future__ import annotations -from typing import Dict, Union, Iterable -from typing_extensions import Literal, Required, Annotated, TypeAlias, TypedDict +from typing import Dict, Union +from typing_extensions import Literal, Required, Annotated, TypedDict from .._utils import PropertyInfo -__all__ = [ - "SessionCreateParams", - "BrowserSettings", - "BrowserSettingsContext", - "BrowserSettingsViewport", - "ProxiesUnionMember1", - "ProxiesUnionMember1BrowserbaseProxyConfig", - "ProxiesUnionMember1BrowserbaseProxyConfigGeolocation", - "ProxiesUnionMember1ExternalProxyConfig", - "ProxiesUnionMember1NoneProxyConfig", -] +__all__ = ["SessionCreateParams", "BrowserSettings", "BrowserSettingsContext", "BrowserSettingsViewport"] class SessionCreateParams(TypedDict, total=False): @@ -47,7 +37,7 @@ class SessionCreateParams(TypedDict, total=False): Can be found in [Settings](https://www.browserbase.com/settings). """ - proxies: Union[bool, Iterable[ProxiesUnionMember1]] + proxies: Union[bool, object] """Proxy configuration. Can be true for default proxy, or an array of proxy configurations. @@ -121,68 +111,3 @@ class BrowserSettings(TypedDict, total=False): """Enable or disable captcha solving in the browser. Defaults to `true`.""" viewport: BrowserSettingsViewport - - -class ProxiesUnionMember1BrowserbaseProxyConfigGeolocation(TypedDict, total=False): - """Configuration for geolocation""" - - country: Required[str] - """Country code in ISO 3166-1 alpha-2 format""" - - city: str - """Name of the city. Use spaces for multi-word city names. Optional.""" - - state: str - """US state code (2 characters). Must also specify US as the country. Optional.""" - - -class ProxiesUnionMember1BrowserbaseProxyConfig(TypedDict, total=False): - type: Required[Literal["browserbase"]] - """Type of proxy. - - Always use 'browserbase' for the Browserbase managed proxy network. - """ - - domain_pattern: Annotated[str, PropertyInfo(alias="domainPattern")] - """Domain pattern for which this proxy should be used. - - If omitted, defaults to all domains. Optional. - """ - - geolocation: ProxiesUnionMember1BrowserbaseProxyConfigGeolocation - """Configuration for geolocation""" - - -class ProxiesUnionMember1ExternalProxyConfig(TypedDict, total=False): - server: Required[str] - """Server URL for external proxy. Required.""" - - type: Required[Literal["external"]] - """Type of proxy. Always 'external' for this config.""" - - domain_pattern: Annotated[str, PropertyInfo(alias="domainPattern")] - """Domain pattern for which this proxy should be used. - - If omitted, defaults to all domains. Optional. - """ - - password: str - """Password for external proxy authentication. Optional.""" - - username: str - """Username for external proxy authentication. Optional.""" - - -class ProxiesUnionMember1NoneProxyConfig(TypedDict, total=False): - domain_pattern: Required[Annotated[str, PropertyInfo(alias="domainPattern")]] - """Domain pattern for which site should have proxies disabled.""" - - type: Required[Literal["none"]] - """Type of proxy. Use 'none' to disable proxy for matching domains.""" - - -ProxiesUnionMember1: TypeAlias = Union[ - ProxiesUnionMember1BrowserbaseProxyConfig, - ProxiesUnionMember1ExternalProxyConfig, - ProxiesUnionMember1NoneProxyConfig, -] From 53ea0f60ecc778575336faf95159fa2bc84db476 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 23 Feb 2026 23:17:22 +0000 Subject: [PATCH 246/330] feat(api): manual updates --- .stats.yml | 4 +- .../resources/sessions/sessions.py | 6 +- .../types/session_create_params.py | 83 ++++++++++++++++++- 3 files changed, 84 insertions(+), 9 deletions(-) diff --git a/.stats.yml b/.stats.yml index 05a7ecdb..2d76bb2b 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 19 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fbrowserbase-a9cd420dd3e871aaaad18fb9def41d277ae000607f369657c965a30e779c221a.yml +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fbrowserbase-af59f86ea3ee682d54560eaf6055df5bebacee560749f5f3cbd9c8ff8b5fa8e5.yml openapi_spec_hash: 72dfdba2efc46c7cbbaa98bd234b72de -config_hash: b32e9f369edd843015c9b9d4fbc3b4f9 +config_hash: 99e5318dc8a8d75839ec565d94276c71 diff --git a/src/browserbase/resources/sessions/sessions.py b/src/browserbase/resources/sessions/sessions.py index e7523a8c..49ab9ce6 100644 --- a/src/browserbase/resources/sessions/sessions.py +++ b/src/browserbase/resources/sessions/sessions.py @@ -2,7 +2,7 @@ from __future__ import annotations -from typing import Dict, Union +from typing import Dict, Union, Iterable from typing_extensions import Literal import httpx @@ -104,7 +104,7 @@ def create( extension_id: str | Omit = omit, keep_alive: bool | Omit = omit, project_id: str | Omit = omit, - proxies: Union[bool, object] | Omit = omit, + proxies: Union[bool, Iterable[session_create_params.ProxiesUnionMember1]] | Omit = omit, region: Literal["us-west-2", "us-east-1", "eu-central-1", "ap-southeast-1"] | Omit = omit, user_metadata: Dict[str, object] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. @@ -373,7 +373,7 @@ async def create( extension_id: str | Omit = omit, keep_alive: bool | Omit = omit, project_id: str | Omit = omit, - proxies: Union[bool, object] | Omit = omit, + proxies: Union[bool, Iterable[session_create_params.ProxiesUnionMember1]] | Omit = omit, region: Literal["us-west-2", "us-east-1", "eu-central-1", "ap-southeast-1"] | Omit = omit, user_metadata: Dict[str, object] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. diff --git a/src/browserbase/types/session_create_params.py b/src/browserbase/types/session_create_params.py index dc495448..24d71f0f 100644 --- a/src/browserbase/types/session_create_params.py +++ b/src/browserbase/types/session_create_params.py @@ -2,12 +2,22 @@ from __future__ import annotations -from typing import Dict, Union -from typing_extensions import Literal, Required, Annotated, TypedDict +from typing import Dict, Union, Iterable +from typing_extensions import Literal, Required, Annotated, TypeAlias, TypedDict from .._utils import PropertyInfo -__all__ = ["SessionCreateParams", "BrowserSettings", "BrowserSettingsContext", "BrowserSettingsViewport"] +__all__ = [ + "SessionCreateParams", + "BrowserSettings", + "BrowserSettingsContext", + "BrowserSettingsViewport", + "ProxiesUnionMember1", + "ProxiesUnionMember1BrowserbaseProxyConfig", + "ProxiesUnionMember1BrowserbaseProxyConfigGeolocation", + "ProxiesUnionMember1ExternalProxyConfig", + "ProxiesUnionMember1NoneProxyConfig", +] class SessionCreateParams(TypedDict, total=False): @@ -37,7 +47,7 @@ class SessionCreateParams(TypedDict, total=False): Can be found in [Settings](https://www.browserbase.com/settings). """ - proxies: Union[bool, object] + proxies: Union[bool, Iterable[ProxiesUnionMember1]] """Proxy configuration. Can be true for default proxy, or an array of proxy configurations. @@ -111,3 +121,68 @@ class BrowserSettings(TypedDict, total=False): """Enable or disable captcha solving in the browser. Defaults to `true`.""" viewport: BrowserSettingsViewport + + +class ProxiesUnionMember1BrowserbaseProxyConfigGeolocation(TypedDict, total=False): + """Configuration for geolocation""" + + country: Required[str] + """Country code in ISO 3166-1 alpha-2 format""" + + city: str + """Name of the city. Use spaces for multi-word city names. Optional.""" + + state: str + """US state code (2 characters). Must also specify US as the country. Optional.""" + + +class ProxiesUnionMember1BrowserbaseProxyConfig(TypedDict, total=False): + type: Required[Literal["browserbase"]] + """Type of proxy. + + Always use 'browserbase' for the Browserbase managed proxy network. + """ + + domain_pattern: Annotated[str, PropertyInfo(alias="domainPattern")] + """Domain pattern for which this proxy should be used. + + If omitted, defaults to all domains. Optional. + """ + + geolocation: ProxiesUnionMember1BrowserbaseProxyConfigGeolocation + """Configuration for geolocation""" + + +class ProxiesUnionMember1ExternalProxyConfig(TypedDict, total=False): + server: Required[str] + """Server URL for external proxy. Required.""" + + type: Required[Literal["external"]] + """Type of proxy. Always 'external' for this config.""" + + domain_pattern: Annotated[str, PropertyInfo(alias="domainPattern")] + """Domain pattern for which this proxy should be used. + + If omitted, defaults to all domains. Optional. + """ + + password: str + """Password for external proxy authentication. Optional.""" + + username: str + """Username for external proxy authentication. Optional.""" + + +class ProxiesUnionMember1NoneProxyConfig(TypedDict, total=False): + domain_pattern: Required[Annotated[str, PropertyInfo(alias="domainPattern")]] + """Domain pattern for which site should have proxies disabled.""" + + type: Required[Literal["none"]] + """Type of proxy. Use 'none' to disable proxy for matching domains.""" + + +ProxiesUnionMember1: TypeAlias = Union[ + ProxiesUnionMember1BrowserbaseProxyConfig, + ProxiesUnionMember1ExternalProxyConfig, + ProxiesUnionMember1NoneProxyConfig, +] From c670e7435cea808c777aacc4bd439ccb0d56b612 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 23 Feb 2026 23:18:17 +0000 Subject: [PATCH 247/330] feat(api): manual updates --- .stats.yml | 4 +-- .../resources/sessions/sessions.py | 4 +-- .../types/session_create_params.py | 30 +++++++++---------- 3 files changed, 19 insertions(+), 19 deletions(-) diff --git a/.stats.yml b/.stats.yml index 2d76bb2b..c5174584 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 19 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fbrowserbase-af59f86ea3ee682d54560eaf6055df5bebacee560749f5f3cbd9c8ff8b5fa8e5.yml +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fbrowserbase-cd8c38feb61fd4aac23600d0e9e51f11db92b1a119d0e807e61835c05a7bf68e.yml openapi_spec_hash: 72dfdba2efc46c7cbbaa98bd234b72de -config_hash: 99e5318dc8a8d75839ec565d94276c71 +config_hash: 2ac55ea7ba4af84e5b7486df92730bcf diff --git a/src/browserbase/resources/sessions/sessions.py b/src/browserbase/resources/sessions/sessions.py index 49ab9ce6..21e369bc 100644 --- a/src/browserbase/resources/sessions/sessions.py +++ b/src/browserbase/resources/sessions/sessions.py @@ -104,7 +104,7 @@ def create( extension_id: str | Omit = omit, keep_alive: bool | Omit = omit, project_id: str | Omit = omit, - proxies: Union[bool, Iterable[session_create_params.ProxiesUnionMember1]] | Omit = omit, + proxies: Union[bool, Iterable[session_create_params.ProxiesUnionArrayVariant1]] | Omit = omit, region: Literal["us-west-2", "us-east-1", "eu-central-1", "ap-southeast-1"] | Omit = omit, user_metadata: Dict[str, object] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. @@ -373,7 +373,7 @@ async def create( extension_id: str | Omit = omit, keep_alive: bool | Omit = omit, project_id: str | Omit = omit, - proxies: Union[bool, Iterable[session_create_params.ProxiesUnionMember1]] | Omit = omit, + proxies: Union[bool, Iterable[session_create_params.ProxiesUnionArrayVariant1]] | Omit = omit, region: Literal["us-west-2", "us-east-1", "eu-central-1", "ap-southeast-1"] | Omit = omit, user_metadata: Dict[str, object] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. diff --git a/src/browserbase/types/session_create_params.py b/src/browserbase/types/session_create_params.py index 24d71f0f..d45b3151 100644 --- a/src/browserbase/types/session_create_params.py +++ b/src/browserbase/types/session_create_params.py @@ -12,11 +12,11 @@ "BrowserSettings", "BrowserSettingsContext", "BrowserSettingsViewport", - "ProxiesUnionMember1", - "ProxiesUnionMember1BrowserbaseProxyConfig", - "ProxiesUnionMember1BrowserbaseProxyConfigGeolocation", - "ProxiesUnionMember1ExternalProxyConfig", - "ProxiesUnionMember1NoneProxyConfig", + "ProxiesUnionArrayVariant1", + "ProxiesUnionArrayVariant1BrowserbaseProxyConfig", + "ProxiesUnionArrayVariant1BrowserbaseProxyConfigGeolocation", + "ProxiesUnionArrayVariant1ExternalProxyConfig", + "ProxiesUnionArrayVariant1NoneProxyConfig", ] @@ -47,7 +47,7 @@ class SessionCreateParams(TypedDict, total=False): Can be found in [Settings](https://www.browserbase.com/settings). """ - proxies: Union[bool, Iterable[ProxiesUnionMember1]] + proxies: Union[bool, Iterable[ProxiesUnionArrayVariant1]] """Proxy configuration. Can be true for default proxy, or an array of proxy configurations. @@ -123,7 +123,7 @@ class BrowserSettings(TypedDict, total=False): viewport: BrowserSettingsViewport -class ProxiesUnionMember1BrowserbaseProxyConfigGeolocation(TypedDict, total=False): +class ProxiesUnionArrayVariant1BrowserbaseProxyConfigGeolocation(TypedDict, total=False): """Configuration for geolocation""" country: Required[str] @@ -136,7 +136,7 @@ class ProxiesUnionMember1BrowserbaseProxyConfigGeolocation(TypedDict, total=Fals """US state code (2 characters). Must also specify US as the country. Optional.""" -class ProxiesUnionMember1BrowserbaseProxyConfig(TypedDict, total=False): +class ProxiesUnionArrayVariant1BrowserbaseProxyConfig(TypedDict, total=False): type: Required[Literal["browserbase"]] """Type of proxy. @@ -149,11 +149,11 @@ class ProxiesUnionMember1BrowserbaseProxyConfig(TypedDict, total=False): If omitted, defaults to all domains. Optional. """ - geolocation: ProxiesUnionMember1BrowserbaseProxyConfigGeolocation + geolocation: ProxiesUnionArrayVariant1BrowserbaseProxyConfigGeolocation """Configuration for geolocation""" -class ProxiesUnionMember1ExternalProxyConfig(TypedDict, total=False): +class ProxiesUnionArrayVariant1ExternalProxyConfig(TypedDict, total=False): server: Required[str] """Server URL for external proxy. Required.""" @@ -173,7 +173,7 @@ class ProxiesUnionMember1ExternalProxyConfig(TypedDict, total=False): """Username for external proxy authentication. Optional.""" -class ProxiesUnionMember1NoneProxyConfig(TypedDict, total=False): +class ProxiesUnionArrayVariant1NoneProxyConfig(TypedDict, total=False): domain_pattern: Required[Annotated[str, PropertyInfo(alias="domainPattern")]] """Domain pattern for which site should have proxies disabled.""" @@ -181,8 +181,8 @@ class ProxiesUnionMember1NoneProxyConfig(TypedDict, total=False): """Type of proxy. Use 'none' to disable proxy for matching domains.""" -ProxiesUnionMember1: TypeAlias = Union[ - ProxiesUnionMember1BrowserbaseProxyConfig, - ProxiesUnionMember1ExternalProxyConfig, - ProxiesUnionMember1NoneProxyConfig, +ProxiesUnionArrayVariant1: TypeAlias = Union[ + ProxiesUnionArrayVariant1BrowserbaseProxyConfig, + ProxiesUnionArrayVariant1ExternalProxyConfig, + ProxiesUnionArrayVariant1NoneProxyConfig, ] From b2b26a3caa3f51fd6ed78dc46c45a206e7eabbe6 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 23 Feb 2026 23:23:02 +0000 Subject: [PATCH 248/330] feat(api): api update --- .stats.yml | 8 +- README.md | 1 + api.md | 29 ++-- src/browserbase/resources/contexts.py | 104 ++------------ src/browserbase/resources/extensions.py | 27 ++-- src/browserbase/resources/projects.py | 32 ++--- .../resources/sessions/sessions.py | 104 +++++++------- src/browserbase/types/__init__.py | 13 +- .../types/context_create_params.py | 4 +- ...ontext.py => context_retrieve_response.py} | 4 +- ...ension.py => extension_create_response.py} | 4 +- .../types/extension_retrieve_response.py | 22 +++ .../types/project_list_response.py | 27 +++- ...roject.py => project_retrieve_response.py} | 4 +- ...ect_usage.py => project_usage_response.py} | 4 +- .../types/session_create_params.py | 98 ++++++++----- ...live_urls.py => session_debug_response.py} | 4 +- .../types/session_list_response.py | 58 +++++++- .../types/session_update_params.py | 12 +- ...{session.py => session_update_response.py} | 4 +- src/browserbase/types/sessions/__init__.py | 2 - .../types/sessions/log_list_response.py | 48 ++++++- .../sessions/recording_retrieve_response.py | 26 +++- src/browserbase/types/sessions/session_log.py | 46 ------ .../types/sessions/session_recording.py | 24 ---- tests/api_resources/test_contexts.py | 120 +++------------- tests/api_resources/test_extensions.py | 26 ++-- tests/api_resources/test_projects.py | 26 ++-- tests/api_resources/test_sessions.py | 136 ++++++++++++------ tests/test_client.py | 28 ++-- 30 files changed, 529 insertions(+), 516 deletions(-) rename src/browserbase/types/{context.py => context_retrieve_response.py} (84%) rename src/browserbase/types/{extension.py => extension_create_response.py} (85%) create mode 100644 src/browserbase/types/extension_retrieve_response.py rename src/browserbase/types/{project.py => project_retrieve_response.py} (87%) rename src/browserbase/types/{project_usage.py => project_usage_response.py} (78%) rename src/browserbase/types/{session_live_urls.py => session_debug_response.py} (88%) rename src/browserbase/types/{session.py => session_update_response.py} (95%) delete mode 100644 src/browserbase/types/sessions/session_log.py delete mode 100644 src/browserbase/types/sessions/session_recording.py diff --git a/.stats.yml b/.stats.yml index c5174584..b000c8c1 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ -configured_endpoints: 19 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fbrowserbase-cd8c38feb61fd4aac23600d0e9e51f11db92b1a119d0e807e61835c05a7bf68e.yml -openapi_spec_hash: 72dfdba2efc46c7cbbaa98bd234b72de -config_hash: 2ac55ea7ba4af84e5b7486df92730bcf +configured_endpoints: 18 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fbrowserbase-be7a4aeebb1605262935b4b3ab446a95b1fad8a7d18098943dd548c8a486ef13.yml +openapi_spec_hash: 1c950a109f80140711e7ae2cf87fddad +config_hash: b3ca4ec5b02e5333af51ebc2e9fdef1b diff --git a/README.md b/README.md index 5d384354..dcb4b768 100644 --- a/README.md +++ b/README.md @@ -122,6 +122,7 @@ from browserbase import Browserbase client = Browserbase() session = client.sessions.create( + project_id="projectId", browser_settings={}, ) print(session.browser_settings) diff --git a/api.md b/api.md index d2d26e57..01454851 100644 --- a/api.md +++ b/api.md @@ -3,28 +3,27 @@ Types: ```python -from browserbase.types import Context, ContextCreateResponse, ContextUpdateResponse +from browserbase.types import ContextCreateResponse, ContextRetrieveResponse, ContextUpdateResponse ``` Methods: - client.contexts.create(\*\*params) -> ContextCreateResponse -- client.contexts.retrieve(id) -> Context +- client.contexts.retrieve(id) -> ContextRetrieveResponse - client.contexts.update(id) -> ContextUpdateResponse -- client.contexts.delete(id) -> None # Extensions Types: ```python -from browserbase.types import Extension +from browserbase.types import ExtensionCreateResponse, ExtensionRetrieveResponse ``` Methods: -- client.extensions.create(\*\*params) -> Extension -- client.extensions.retrieve(id) -> Extension +- client.extensions.create(\*\*params) -> ExtensionCreateResponse +- client.extensions.retrieve(id) -> ExtensionRetrieveResponse - client.extensions.delete(id) -> None # Projects @@ -32,14 +31,14 @@ Methods: Types: ```python -from browserbase.types import Project, ProjectUsage, ProjectListResponse +from browserbase.types import ProjectRetrieveResponse, ProjectListResponse, ProjectUsageResponse ``` Methods: -- client.projects.retrieve(id) -> Project +- client.projects.retrieve(id) -> ProjectRetrieveResponse - client.projects.list() -> ProjectListResponse -- client.projects.usage(id) -> ProjectUsage +- client.projects.usage(id) -> ProjectUsageResponse # Sessions @@ -47,11 +46,11 @@ Types: ```python from browserbase.types import ( - Session, - SessionLiveURLs, SessionCreateResponse, SessionRetrieveResponse, + SessionUpdateResponse, SessionListResponse, + SessionDebugResponse, ) ``` @@ -59,9 +58,9 @@ Methods: - client.sessions.create(\*\*params) -> SessionCreateResponse - client.sessions.retrieve(id) -> SessionRetrieveResponse -- client.sessions.update(id, \*\*params) -> Session +- client.sessions.update(id, \*\*params) -> SessionUpdateResponse - client.sessions.list(\*\*params) -> SessionListResponse -- client.sessions.debug(id) -> SessionLiveURLs +- client.sessions.debug(id) -> SessionDebugResponse ## Downloads @@ -74,7 +73,7 @@ Methods: Types: ```python -from browserbase.types.sessions import SessionLog, LogListResponse +from browserbase.types.sessions import LogListResponse ``` Methods: @@ -86,7 +85,7 @@ Methods: Types: ```python -from browserbase.types.sessions import SessionRecording, RecordingRetrieveResponse +from browserbase.types.sessions import RecordingRetrieveResponse ``` Methods: diff --git a/src/browserbase/resources/contexts.py b/src/browserbase/resources/contexts.py index 03af85f0..d2bb4167 100644 --- a/src/browserbase/resources/contexts.py +++ b/src/browserbase/resources/contexts.py @@ -5,7 +5,7 @@ import httpx from ..types import context_create_params -from .._types import Body, Omit, Query, Headers, NoneType, NotGiven, omit, not_given +from .._types import Body, Query, Headers, NotGiven, not_given from .._utils import maybe_transform, async_maybe_transform from .._compat import cached_property from .._resource import SyncAPIResource, AsyncAPIResource @@ -16,9 +16,9 @@ async_to_streamed_response_wrapper, ) from .._base_client import make_request_options -from ..types.context import Context from ..types.context_create_response import ContextCreateResponse from ..types.context_update_response import ContextUpdateResponse +from ..types.context_retrieve_response import ContextRetrieveResponse __all__ = ["ContextsResource", "AsyncContextsResource"] @@ -46,7 +46,7 @@ def with_streaming_response(self) -> ContextsResourceWithStreamingResponse: def create( self, *, - project_id: str | Omit = omit, + project_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, @@ -89,9 +89,9 @@ def retrieve( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> Context: + ) -> ContextRetrieveResponse: """ - Context + Get a Context Args: extra_headers: Send extra headers @@ -109,7 +109,7 @@ def retrieve( options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=Context, + cast_to=ContextRetrieveResponse, ) def update( @@ -124,7 +124,7 @@ def update( timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> ContextUpdateResponse: """ - Update Context + Update a Context Args: extra_headers: Send extra headers @@ -145,40 +145,6 @@ def update( cast_to=ContextUpdateResponse, ) - def delete( - self, - 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, - ) -> None: - """ - Delete Context - - 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 id: - raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") - extra_headers = {"Accept": "*/*", **(extra_headers or {})} - return self._delete( - f"/v1/contexts/{id}", - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=NoneType, - ) - class AsyncContextsResource(AsyncAPIResource): @cached_property @@ -203,7 +169,7 @@ def with_streaming_response(self) -> AsyncContextsResourceWithStreamingResponse: async def create( self, *, - project_id: str | Omit = omit, + project_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, @@ -246,9 +212,9 @@ async def retrieve( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> Context: + ) -> ContextRetrieveResponse: """ - Context + Get a Context Args: extra_headers: Send extra headers @@ -266,7 +232,7 @@ async def retrieve( options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=Context, + cast_to=ContextRetrieveResponse, ) async def update( @@ -281,7 +247,7 @@ async def update( timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> ContextUpdateResponse: """ - Update Context + Update a Context Args: extra_headers: Send extra headers @@ -302,40 +268,6 @@ async def update( cast_to=ContextUpdateResponse, ) - async def delete( - self, - 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, - ) -> None: - """ - Delete Context - - 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 id: - raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") - extra_headers = {"Accept": "*/*", **(extra_headers or {})} - return await self._delete( - f"/v1/contexts/{id}", - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=NoneType, - ) - class ContextsResourceWithRawResponse: def __init__(self, contexts: ContextsResource) -> None: @@ -350,9 +282,6 @@ def __init__(self, contexts: ContextsResource) -> None: self.update = to_raw_response_wrapper( contexts.update, ) - self.delete = to_raw_response_wrapper( - contexts.delete, - ) class AsyncContextsResourceWithRawResponse: @@ -368,9 +297,6 @@ def __init__(self, contexts: AsyncContextsResource) -> None: self.update = async_to_raw_response_wrapper( contexts.update, ) - self.delete = async_to_raw_response_wrapper( - contexts.delete, - ) class ContextsResourceWithStreamingResponse: @@ -386,9 +312,6 @@ def __init__(self, contexts: ContextsResource) -> None: self.update = to_streamed_response_wrapper( contexts.update, ) - self.delete = to_streamed_response_wrapper( - contexts.delete, - ) class AsyncContextsResourceWithStreamingResponse: @@ -404,6 +327,3 @@ def __init__(self, contexts: AsyncContextsResource) -> None: self.update = async_to_streamed_response_wrapper( contexts.update, ) - self.delete = async_to_streamed_response_wrapper( - contexts.delete, - ) diff --git a/src/browserbase/resources/extensions.py b/src/browserbase/resources/extensions.py index 882a495f..21d06e70 100644 --- a/src/browserbase/resources/extensions.py +++ b/src/browserbase/resources/extensions.py @@ -18,7 +18,8 @@ async_to_streamed_response_wrapper, ) from .._base_client import make_request_options -from ..types.extension import Extension +from ..types.extension_create_response import ExtensionCreateResponse +from ..types.extension_retrieve_response import ExtensionRetrieveResponse __all__ = ["ExtensionsResource", "AsyncExtensionsResource"] @@ -53,7 +54,7 @@ def create( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> Extension: + ) -> ExtensionCreateResponse: """ Upload an Extension @@ -79,7 +80,7 @@ def create( options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=Extension, + cast_to=ExtensionCreateResponse, ) def retrieve( @@ -92,9 +93,9 @@ def retrieve( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> Extension: + ) -> ExtensionRetrieveResponse: """ - Extension + Get an Extension Args: extra_headers: Send extra headers @@ -112,7 +113,7 @@ def retrieve( options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=Extension, + cast_to=ExtensionRetrieveResponse, ) def delete( @@ -127,7 +128,7 @@ def delete( timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> None: """ - Delete Extension + Delete an Extension Args: extra_headers: Send extra headers @@ -180,7 +181,7 @@ async def create( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> Extension: + ) -> ExtensionCreateResponse: """ Upload an Extension @@ -206,7 +207,7 @@ async def create( options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=Extension, + cast_to=ExtensionCreateResponse, ) async def retrieve( @@ -219,9 +220,9 @@ async def retrieve( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> Extension: + ) -> ExtensionRetrieveResponse: """ - Extension + Get an Extension Args: extra_headers: Send extra headers @@ -239,7 +240,7 @@ async def retrieve( options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=Extension, + cast_to=ExtensionRetrieveResponse, ) async def delete( @@ -254,7 +255,7 @@ async def delete( timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> None: """ - Delete Extension + Delete an Extension Args: extra_headers: Send extra headers diff --git a/src/browserbase/resources/projects.py b/src/browserbase/resources/projects.py index a6ae6338..62c28afa 100644 --- a/src/browserbase/resources/projects.py +++ b/src/browserbase/resources/projects.py @@ -14,9 +14,9 @@ async_to_streamed_response_wrapper, ) from .._base_client import make_request_options -from ..types.project import Project -from ..types.project_usage import ProjectUsage from ..types.project_list_response import ProjectListResponse +from ..types.project_usage_response import ProjectUsageResponse +from ..types.project_retrieve_response import ProjectRetrieveResponse __all__ = ["ProjectsResource", "AsyncProjectsResource"] @@ -51,9 +51,9 @@ def retrieve( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> Project: + ) -> ProjectRetrieveResponse: """ - Project + Get a Project Args: extra_headers: Send extra headers @@ -71,7 +71,7 @@ def retrieve( options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=Project, + cast_to=ProjectRetrieveResponse, ) def list( @@ -84,7 +84,7 @@ def list( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> ProjectListResponse: - """List projects""" + """List Projects""" return self._get( "/v1/projects", options=make_request_options( @@ -103,9 +103,9 @@ def usage( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> ProjectUsage: + ) -> ProjectUsageResponse: """ - Project Usage + Get Project Usage Args: extra_headers: Send extra headers @@ -123,7 +123,7 @@ def usage( options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=ProjectUsage, + cast_to=ProjectUsageResponse, ) @@ -157,9 +157,9 @@ async def retrieve( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> Project: + ) -> ProjectRetrieveResponse: """ - Project + Get a Project Args: extra_headers: Send extra headers @@ -177,7 +177,7 @@ async def retrieve( options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=Project, + cast_to=ProjectRetrieveResponse, ) async def list( @@ -190,7 +190,7 @@ async def list( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> ProjectListResponse: - """List projects""" + """List Projects""" return await self._get( "/v1/projects", options=make_request_options( @@ -209,9 +209,9 @@ async def usage( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> ProjectUsage: + ) -> ProjectUsageResponse: """ - Project Usage + Get Project Usage Args: extra_headers: Send extra headers @@ -229,7 +229,7 @@ async def usage( options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=ProjectUsage, + cast_to=ProjectUsageResponse, ) diff --git a/src/browserbase/resources/sessions/sessions.py b/src/browserbase/resources/sessions/sessions.py index 21e369bc..ceaaeb81 100644 --- a/src/browserbase/resources/sessions/sessions.py +++ b/src/browserbase/resources/sessions/sessions.py @@ -51,10 +51,10 @@ async_to_streamed_response_wrapper, ) from ..._base_client import make_request_options -from ...types.session import Session -from ...types.session_live_urls import SessionLiveURLs from ...types.session_list_response import SessionListResponse +from ...types.session_debug_response import SessionDebugResponse from ...types.session_create_response import SessionCreateResponse +from ...types.session_update_response import SessionUpdateResponse from ...types.session_retrieve_response import SessionRetrieveResponse __all__ = ["SessionsResource", "AsyncSessionsResource"] @@ -99,13 +99,13 @@ def with_streaming_response(self) -> SessionsResourceWithStreamingResponse: def create( self, *, - api_timeout: int | Omit = omit, + project_id: str, browser_settings: session_create_params.BrowserSettings | Omit = omit, extension_id: str | Omit = omit, keep_alive: bool | Omit = omit, - project_id: str | Omit = omit, - proxies: Union[bool, Iterable[session_create_params.ProxiesUnionArrayVariant1]] | Omit = omit, + proxies: Union[Iterable[session_create_params.ProxiesUnionMember0], bool] | Omit = omit, region: Literal["us-west-2", "us-east-1", "eu-central-1", "ap-southeast-1"] | Omit = omit, + api_timeout: int | Omit = omit, user_metadata: Dict[str, 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. @@ -114,12 +114,13 @@ def create( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> SessionCreateResponse: - """ - Create a Session + """Create a Session Args: - api_timeout: Duration in seconds after which the session will automatically end. Defaults to - the Project's `defaultTimeout`. + project_id: The Project ID. + + Can be found in + [Settings](https://www.browserbase.com/settings). extension_id: The uploaded Extension ID. See [Upload Extension](/reference/api/upload-an-extension). @@ -127,14 +128,14 @@ def create( keep_alive: Set to true to keep the session alive even after disconnections. Available on the Hobby Plan and above. - project_id: The Project ID. Can be found in - [Settings](https://www.browserbase.com/settings). - proxies: Proxy configuration. Can be true for default proxy, or an array of proxy configurations. region: The region where the Session should run. + api_timeout: Duration in seconds after which the session will automatically end. Defaults to + the Project's `defaultTimeout`. + user_metadata: Arbitrary user metadata to attach to the session. To learn more about user metadata, see [User Metadata](/features/sessions#user-metadata). @@ -150,13 +151,13 @@ def create( "/v1/sessions", body=maybe_transform( { - "api_timeout": api_timeout, + "project_id": project_id, "browser_settings": browser_settings, "extension_id": extension_id, "keep_alive": keep_alive, - "project_id": project_id, "proxies": proxies, "region": region, + "api_timeout": api_timeout, "user_metadata": user_metadata, }, session_create_params.SessionCreateParams, @@ -179,7 +180,7 @@ def retrieve( timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> SessionRetrieveResponse: """ - Session + Get a Session Args: extra_headers: Send extra headers @@ -204,25 +205,26 @@ def update( self, id: str, *, + project_id: str, status: Literal["REQUEST_RELEASE"], - project_id: str | 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, - ) -> Session: - """ - Update Session + ) -> SessionUpdateResponse: + """Update a Session Args: - status: Set to `REQUEST_RELEASE` to request that the session complete. Use before - session's timeout to avoid additional charges. + project_id: The Project ID. - project_id: The Project ID. Can be found in + Can be found in [Settings](https://www.browserbase.com/settings). + status: Set to `REQUEST_RELEASE` to request that the session complete. Use before + session's timeout to avoid additional charges. + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -237,15 +239,15 @@ def update( f"/v1/sessions/{id}", body=maybe_transform( { - "status": status, "project_id": project_id, + "status": status, }, session_update_params.SessionUpdateParams, ), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=Session, + cast_to=SessionUpdateResponse, ) def list( @@ -305,7 +307,7 @@ def debug( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> SessionLiveURLs: + ) -> SessionDebugResponse: """ Session Live URLs @@ -325,7 +327,7 @@ def debug( options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=SessionLiveURLs, + cast_to=SessionDebugResponse, ) @@ -368,13 +370,13 @@ def with_streaming_response(self) -> AsyncSessionsResourceWithStreamingResponse: async def create( self, *, - api_timeout: int | Omit = omit, + project_id: str, browser_settings: session_create_params.BrowserSettings | Omit = omit, extension_id: str | Omit = omit, keep_alive: bool | Omit = omit, - project_id: str | Omit = omit, - proxies: Union[bool, Iterable[session_create_params.ProxiesUnionArrayVariant1]] | Omit = omit, + proxies: Union[Iterable[session_create_params.ProxiesUnionMember0], bool] | Omit = omit, region: Literal["us-west-2", "us-east-1", "eu-central-1", "ap-southeast-1"] | Omit = omit, + api_timeout: int | Omit = omit, user_metadata: Dict[str, 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. @@ -383,12 +385,13 @@ async def create( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> SessionCreateResponse: - """ - Create a Session + """Create a Session Args: - api_timeout: Duration in seconds after which the session will automatically end. Defaults to - the Project's `defaultTimeout`. + project_id: The Project ID. + + Can be found in + [Settings](https://www.browserbase.com/settings). extension_id: The uploaded Extension ID. See [Upload Extension](/reference/api/upload-an-extension). @@ -396,14 +399,14 @@ async def create( keep_alive: Set to true to keep the session alive even after disconnections. Available on the Hobby Plan and above. - project_id: The Project ID. Can be found in - [Settings](https://www.browserbase.com/settings). - proxies: Proxy configuration. Can be true for default proxy, or an array of proxy configurations. region: The region where the Session should run. + api_timeout: Duration in seconds after which the session will automatically end. Defaults to + the Project's `defaultTimeout`. + user_metadata: Arbitrary user metadata to attach to the session. To learn more about user metadata, see [User Metadata](/features/sessions#user-metadata). @@ -419,13 +422,13 @@ async def create( "/v1/sessions", body=await async_maybe_transform( { - "api_timeout": api_timeout, + "project_id": project_id, "browser_settings": browser_settings, "extension_id": extension_id, "keep_alive": keep_alive, - "project_id": project_id, "proxies": proxies, "region": region, + "api_timeout": api_timeout, "user_metadata": user_metadata, }, session_create_params.SessionCreateParams, @@ -448,7 +451,7 @@ async def retrieve( timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> SessionRetrieveResponse: """ - Session + Get a Session Args: extra_headers: Send extra headers @@ -473,25 +476,26 @@ async def update( self, id: str, *, + project_id: str, status: Literal["REQUEST_RELEASE"], - project_id: str | 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, - ) -> Session: - """ - Update Session + ) -> SessionUpdateResponse: + """Update a Session Args: - status: Set to `REQUEST_RELEASE` to request that the session complete. Use before - session's timeout to avoid additional charges. + project_id: The Project ID. - project_id: The Project ID. Can be found in + Can be found in [Settings](https://www.browserbase.com/settings). + status: Set to `REQUEST_RELEASE` to request that the session complete. Use before + session's timeout to avoid additional charges. + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -506,15 +510,15 @@ async def update( f"/v1/sessions/{id}", body=await async_maybe_transform( { - "status": status, "project_id": project_id, + "status": status, }, session_update_params.SessionUpdateParams, ), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=Session, + cast_to=SessionUpdateResponse, ) async def list( @@ -574,7 +578,7 @@ async def debug( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> SessionLiveURLs: + ) -> SessionDebugResponse: """ Session Live URLs @@ -594,7 +598,7 @@ async def debug( options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=SessionLiveURLs, + cast_to=SessionDebugResponse, ) diff --git a/src/browserbase/types/__init__.py b/src/browserbase/types/__init__.py index 4dd85ddb..20e2f905 100644 --- a/src/browserbase/types/__init__.py +++ b/src/browserbase/types/__init__.py @@ -2,20 +2,21 @@ from __future__ import annotations -from .context import Context as Context -from .project import Project as Project -from .session import Session as Session -from .extension import Extension as Extension -from .project_usage import ProjectUsage as ProjectUsage -from .session_live_urls import SessionLiveURLs as SessionLiveURLs from .session_list_params import SessionListParams as SessionListParams from .context_create_params import ContextCreateParams as ContextCreateParams from .project_list_response import ProjectListResponse as ProjectListResponse from .session_create_params import SessionCreateParams as SessionCreateParams from .session_list_response import SessionListResponse as SessionListResponse from .session_update_params import SessionUpdateParams as SessionUpdateParams +from .project_usage_response import ProjectUsageResponse as ProjectUsageResponse +from .session_debug_response import SessionDebugResponse as SessionDebugResponse from .context_create_response import ContextCreateResponse as ContextCreateResponse from .context_update_response import ContextUpdateResponse as ContextUpdateResponse from .extension_create_params import ExtensionCreateParams as ExtensionCreateParams from .session_create_response import SessionCreateResponse as SessionCreateResponse +from .session_update_response import SessionUpdateResponse as SessionUpdateResponse +from .context_retrieve_response import ContextRetrieveResponse as ContextRetrieveResponse +from .extension_create_response import ExtensionCreateResponse as ExtensionCreateResponse +from .project_retrieve_response import ProjectRetrieveResponse as ProjectRetrieveResponse from .session_retrieve_response import SessionRetrieveResponse as SessionRetrieveResponse +from .extension_retrieve_response import ExtensionRetrieveResponse as ExtensionRetrieveResponse diff --git a/src/browserbase/types/context_create_params.py b/src/browserbase/types/context_create_params.py index 66c6c468..75cd1fcd 100644 --- a/src/browserbase/types/context_create_params.py +++ b/src/browserbase/types/context_create_params.py @@ -2,7 +2,7 @@ from __future__ import annotations -from typing_extensions import Annotated, TypedDict +from typing_extensions import Required, Annotated, TypedDict from .._utils import PropertyInfo @@ -10,7 +10,7 @@ class ContextCreateParams(TypedDict, total=False): - project_id: Annotated[str, PropertyInfo(alias="projectId")] + project_id: Required[Annotated[str, PropertyInfo(alias="projectId")]] """The Project ID. Can be found in [Settings](https://www.browserbase.com/settings). diff --git a/src/browserbase/types/context.py b/src/browserbase/types/context_retrieve_response.py similarity index 84% rename from src/browserbase/types/context.py rename to src/browserbase/types/context_retrieve_response.py index cb5c32fd..c2cd6925 100644 --- a/src/browserbase/types/context.py +++ b/src/browserbase/types/context_retrieve_response.py @@ -6,10 +6,10 @@ from .._models import BaseModel -__all__ = ["Context"] +__all__ = ["ContextRetrieveResponse"] -class Context(BaseModel): +class ContextRetrieveResponse(BaseModel): id: str created_at: datetime = FieldInfo(alias="createdAt") diff --git a/src/browserbase/types/extension.py b/src/browserbase/types/extension_create_response.py similarity index 85% rename from src/browserbase/types/extension.py rename to src/browserbase/types/extension_create_response.py index 94582c34..d2b74f41 100644 --- a/src/browserbase/types/extension.py +++ b/src/browserbase/types/extension_create_response.py @@ -6,10 +6,10 @@ from .._models import BaseModel -__all__ = ["Extension"] +__all__ = ["ExtensionCreateResponse"] -class Extension(BaseModel): +class ExtensionCreateResponse(BaseModel): id: str created_at: datetime = FieldInfo(alias="createdAt") diff --git a/src/browserbase/types/extension_retrieve_response.py b/src/browserbase/types/extension_retrieve_response.py new file mode 100644 index 00000000..c786348e --- /dev/null +++ b/src/browserbase/types/extension_retrieve_response.py @@ -0,0 +1,22 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from datetime import datetime + +from pydantic import Field as FieldInfo + +from .._models import BaseModel + +__all__ = ["ExtensionRetrieveResponse"] + + +class ExtensionRetrieveResponse(BaseModel): + id: str + + created_at: datetime = FieldInfo(alias="createdAt") + + file_name: str = FieldInfo(alias="fileName") + + project_id: str = FieldInfo(alias="projectId") + """The Project ID linked to the uploaded Extension.""" + + updated_at: datetime = FieldInfo(alias="updatedAt") diff --git a/src/browserbase/types/project_list_response.py b/src/browserbase/types/project_list_response.py index 2d05a236..e364b520 100644 --- a/src/browserbase/types/project_list_response.py +++ b/src/browserbase/types/project_list_response.py @@ -1,10 +1,31 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. from typing import List +from datetime import datetime from typing_extensions import TypeAlias -from .project import Project +from pydantic import Field as FieldInfo -__all__ = ["ProjectListResponse"] +from .._models import BaseModel -ProjectListResponse: TypeAlias = List[Project] +__all__ = ["ProjectListResponse", "ProjectListResponseItem"] + + +class ProjectListResponseItem(BaseModel): + id: str + + concurrency: int + """The maximum number of sessions that this project can run concurrently.""" + + created_at: datetime = FieldInfo(alias="createdAt") + + default_timeout: int = FieldInfo(alias="defaultTimeout") + + name: str + + owner_id: str = FieldInfo(alias="ownerId") + + updated_at: datetime = FieldInfo(alias="updatedAt") + + +ProjectListResponse: TypeAlias = List[ProjectListResponseItem] diff --git a/src/browserbase/types/project.py b/src/browserbase/types/project_retrieve_response.py similarity index 87% rename from src/browserbase/types/project.py rename to src/browserbase/types/project_retrieve_response.py index dc3cf335..78126679 100644 --- a/src/browserbase/types/project.py +++ b/src/browserbase/types/project_retrieve_response.py @@ -6,10 +6,10 @@ from .._models import BaseModel -__all__ = ["Project"] +__all__ = ["ProjectRetrieveResponse"] -class Project(BaseModel): +class ProjectRetrieveResponse(BaseModel): id: str concurrency: int diff --git a/src/browserbase/types/project_usage.py b/src/browserbase/types/project_usage_response.py similarity index 78% rename from src/browserbase/types/project_usage.py rename to src/browserbase/types/project_usage_response.py index c8a03f5b..b52fccfe 100644 --- a/src/browserbase/types/project_usage.py +++ b/src/browserbase/types/project_usage_response.py @@ -4,10 +4,10 @@ from .._models import BaseModel -__all__ = ["ProjectUsage"] +__all__ = ["ProjectUsageResponse"] -class ProjectUsage(BaseModel): +class ProjectUsageResponse(BaseModel): browser_minutes: int = FieldInfo(alias="browserMinutes") proxy_bytes: int = FieldInfo(alias="proxyBytes") diff --git a/src/browserbase/types/session_create_params.py b/src/browserbase/types/session_create_params.py index d45b3151..2ba36400 100644 --- a/src/browserbase/types/session_create_params.py +++ b/src/browserbase/types/session_create_params.py @@ -2,29 +2,31 @@ from __future__ import annotations -from typing import Dict, Union, Iterable +from typing import Dict, List, Union, Iterable from typing_extensions import Literal, Required, Annotated, TypeAlias, TypedDict +from .._types import SequenceNotStr from .._utils import PropertyInfo __all__ = [ "SessionCreateParams", "BrowserSettings", "BrowserSettingsContext", + "BrowserSettingsFingerprint", + "BrowserSettingsFingerprintScreen", "BrowserSettingsViewport", - "ProxiesUnionArrayVariant1", - "ProxiesUnionArrayVariant1BrowserbaseProxyConfig", - "ProxiesUnionArrayVariant1BrowserbaseProxyConfigGeolocation", - "ProxiesUnionArrayVariant1ExternalProxyConfig", - "ProxiesUnionArrayVariant1NoneProxyConfig", + "ProxiesUnionMember0", + "ProxiesUnionMember0UnionMember0", + "ProxiesUnionMember0UnionMember0Geolocation", + "ProxiesUnionMember0UnionMember1", ] class SessionCreateParams(TypedDict, total=False): - api_timeout: int - """Duration in seconds after which the session will automatically end. + project_id: Required[Annotated[str, PropertyInfo(alias="projectId")]] + """The Project ID. - Defaults to the Project's `defaultTimeout`. + Can be found in [Settings](https://www.browserbase.com/settings). """ browser_settings: Annotated[BrowserSettings, PropertyInfo(alias="browserSettings")] @@ -41,13 +43,7 @@ class SessionCreateParams(TypedDict, total=False): Available on the Hobby Plan and above. """ - project_id: Annotated[str, PropertyInfo(alias="projectId")] - """The Project ID. - - Can be found in [Settings](https://www.browserbase.com/settings). - """ - - proxies: Union[bool, Iterable[ProxiesUnionArrayVariant1]] + proxies: Union[Iterable[ProxiesUnionMember0], bool] """Proxy configuration. Can be true for default proxy, or an array of proxy configurations. @@ -56,6 +52,12 @@ class SessionCreateParams(TypedDict, total=False): region: Literal["us-west-2", "us-east-1", "eu-central-1", "ap-southeast-1"] """The region where the Session should run.""" + api_timeout: Annotated[int, PropertyInfo(alias="timeout")] + """Duration in seconds after which the session will automatically end. + + Defaults to the Project's `defaultTimeout`. + """ + user_metadata: Annotated[Dict[str, object], PropertyInfo(alias="userMetadata")] """Arbitrary user metadata to attach to the session. @@ -72,10 +74,42 @@ class BrowserSettingsContext(TypedDict, total=False): """Whether or not to persist the context after browsing. Defaults to `false`.""" +class BrowserSettingsFingerprintScreen(TypedDict, total=False): + max_height: Annotated[int, PropertyInfo(alias="maxHeight")] + + max_width: Annotated[int, PropertyInfo(alias="maxWidth")] + + min_height: Annotated[int, PropertyInfo(alias="minHeight")] + + min_width: Annotated[int, PropertyInfo(alias="minWidth")] + + +class BrowserSettingsFingerprint(TypedDict, total=False): + """ + See usage examples [on the Stealth Mode page](/features/stealth-mode#fingerprinting) + """ + + 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: BrowserSettingsFingerprintScreen + + class BrowserSettingsViewport(TypedDict, total=False): height: int + """The height of the browser.""" width: int + """The width of the browser.""" class BrowserSettings(TypedDict, total=False): @@ -105,6 +139,12 @@ class BrowserSettings(TypedDict, total=False): See [Upload Extension](/reference/api/upload-an-extension). """ + fingerprint: BrowserSettingsFingerprint + """ + See usage examples + [on the Stealth Mode page](/features/stealth-mode#fingerprinting) + """ + log_session: Annotated[bool, PropertyInfo(alias="logSession")] """Enable or disable session logging. Defaults to `true`.""" @@ -123,8 +163,8 @@ class BrowserSettings(TypedDict, total=False): viewport: BrowserSettingsViewport -class ProxiesUnionArrayVariant1BrowserbaseProxyConfigGeolocation(TypedDict, total=False): - """Configuration for geolocation""" +class ProxiesUnionMember0UnionMember0Geolocation(TypedDict, total=False): + """Geographic location for the proxy. Optional.""" country: Required[str] """Country code in ISO 3166-1 alpha-2 format""" @@ -136,7 +176,7 @@ class ProxiesUnionArrayVariant1BrowserbaseProxyConfigGeolocation(TypedDict, tota """US state code (2 characters). Must also specify US as the country. Optional.""" -class ProxiesUnionArrayVariant1BrowserbaseProxyConfig(TypedDict, total=False): +class ProxiesUnionMember0UnionMember0(TypedDict, total=False): type: Required[Literal["browserbase"]] """Type of proxy. @@ -149,11 +189,11 @@ class ProxiesUnionArrayVariant1BrowserbaseProxyConfig(TypedDict, total=False): If omitted, defaults to all domains. Optional. """ - geolocation: ProxiesUnionArrayVariant1BrowserbaseProxyConfigGeolocation - """Configuration for geolocation""" + geolocation: ProxiesUnionMember0UnionMember0Geolocation + """Geographic location for the proxy. Optional.""" -class ProxiesUnionArrayVariant1ExternalProxyConfig(TypedDict, total=False): +class ProxiesUnionMember0UnionMember1(TypedDict, total=False): server: Required[str] """Server URL for external proxy. Required.""" @@ -173,16 +213,4 @@ class ProxiesUnionArrayVariant1ExternalProxyConfig(TypedDict, total=False): """Username for external proxy authentication. Optional.""" -class ProxiesUnionArrayVariant1NoneProxyConfig(TypedDict, total=False): - domain_pattern: Required[Annotated[str, PropertyInfo(alias="domainPattern")]] - """Domain pattern for which site should have proxies disabled.""" - - type: Required[Literal["none"]] - """Type of proxy. Use 'none' to disable proxy for matching domains.""" - - -ProxiesUnionArrayVariant1: TypeAlias = Union[ - ProxiesUnionArrayVariant1BrowserbaseProxyConfig, - ProxiesUnionArrayVariant1ExternalProxyConfig, - ProxiesUnionArrayVariant1NoneProxyConfig, -] +ProxiesUnionMember0: TypeAlias = Union[ProxiesUnionMember0UnionMember0, ProxiesUnionMember0UnionMember1] diff --git a/src/browserbase/types/session_live_urls.py b/src/browserbase/types/session_debug_response.py similarity index 88% rename from src/browserbase/types/session_live_urls.py rename to src/browserbase/types/session_debug_response.py index 3c7ba320..9cee7a77 100644 --- a/src/browserbase/types/session_live_urls.py +++ b/src/browserbase/types/session_debug_response.py @@ -6,7 +6,7 @@ from .._models import BaseModel -__all__ = ["SessionLiveURLs", "Page"] +__all__ = ["SessionDebugResponse", "Page"] class Page(BaseModel): @@ -23,7 +23,7 @@ class Page(BaseModel): url: str -class SessionLiveURLs(BaseModel): +class SessionDebugResponse(BaseModel): debugger_fullscreen_url: str = FieldInfo(alias="debuggerFullscreenUrl") debugger_url: str = FieldInfo(alias="debuggerUrl") diff --git a/src/browserbase/types/session_list_response.py b/src/browserbase/types/session_list_response.py index ca162ddb..4c1bd885 100644 --- a/src/browserbase/types/session_list_response.py +++ b/src/browserbase/types/session_list_response.py @@ -1,10 +1,58 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -from typing import List -from typing_extensions import TypeAlias +from typing import Dict, List, Optional +from datetime import datetime +from typing_extensions import Literal, TypeAlias -from .session import Session +from pydantic import Field as FieldInfo -__all__ = ["SessionListResponse"] +from .._models import BaseModel -SessionListResponse: TypeAlias = List[Session] +__all__ = ["SessionListResponse", "SessionListResponseItem"] + + +class SessionListResponseItem(BaseModel): + id: str + + created_at: datetime = FieldInfo(alias="createdAt") + + expires_at: datetime = FieldInfo(alias="expiresAt") + + keep_alive: bool = FieldInfo(alias="keepAlive") + """Indicates if the Session was created to be kept alive upon disconnections""" + + project_id: str = FieldInfo(alias="projectId") + """The Project ID linked to the Session.""" + + proxy_bytes: int = FieldInfo(alias="proxyBytes") + """Bytes used via the [Proxy](/features/stealth-mode#proxies-and-residential-ips)""" + + region: Literal["us-west-2", "us-east-1", "eu-central-1", "ap-southeast-1"] + """The region where the Session is running.""" + + started_at: datetime = FieldInfo(alias="startedAt") + + status: Literal["RUNNING", "ERROR", "TIMED_OUT", "COMPLETED"] + + updated_at: datetime = FieldInfo(alias="updatedAt") + + avg_cpu_usage: Optional[int] = FieldInfo(alias="avgCpuUsage", default=None) + """CPU used by the Session""" + + context_id: Optional[str] = FieldInfo(alias="contextId", default=None) + """Optional. The Context linked to the Session.""" + + ended_at: Optional[datetime] = FieldInfo(alias="endedAt", default=None) + + memory_usage: Optional[int] = FieldInfo(alias="memoryUsage", default=None) + """Memory used by the Session""" + + user_metadata: Optional[Dict[str, object]] = FieldInfo(alias="userMetadata", default=None) + """Arbitrary user metadata to attach to the session. + + To learn more about user metadata, see + [User Metadata](/features/sessions#user-metadata). + """ + + +SessionListResponse: TypeAlias = List[SessionListResponseItem] diff --git a/src/browserbase/types/session_update_params.py b/src/browserbase/types/session_update_params.py index 71c589d9..66dcd351 100644 --- a/src/browserbase/types/session_update_params.py +++ b/src/browserbase/types/session_update_params.py @@ -10,14 +10,14 @@ class SessionUpdateParams(TypedDict, total=False): - status: Required[Literal["REQUEST_RELEASE"]] - """Set to `REQUEST_RELEASE` to request that the session complete. + project_id: Required[Annotated[str, PropertyInfo(alias="projectId")]] + """The Project ID. - Use before session's timeout to avoid additional charges. + Can be found in [Settings](https://www.browserbase.com/settings). """ - project_id: Annotated[str, PropertyInfo(alias="projectId")] - """The Project ID. + status: Required[Literal["REQUEST_RELEASE"]] + """Set to `REQUEST_RELEASE` to request that the session complete. - Can be found in [Settings](https://www.browserbase.com/settings). + Use before session's timeout to avoid additional charges. """ diff --git a/src/browserbase/types/session.py b/src/browserbase/types/session_update_response.py similarity index 95% rename from src/browserbase/types/session.py rename to src/browserbase/types/session_update_response.py index 16450e29..67a13711 100644 --- a/src/browserbase/types/session.py +++ b/src/browserbase/types/session_update_response.py @@ -8,10 +8,10 @@ from .._models import BaseModel -__all__ = ["Session"] +__all__ = ["SessionUpdateResponse"] -class Session(BaseModel): +class SessionUpdateResponse(BaseModel): id: str created_at: datetime = FieldInfo(alias="createdAt") diff --git a/src/browserbase/types/sessions/__init__.py b/src/browserbase/types/sessions/__init__.py index 0cef6b19..69d54703 100644 --- a/src/browserbase/types/sessions/__init__.py +++ b/src/browserbase/types/sessions/__init__.py @@ -2,9 +2,7 @@ from __future__ import annotations -from .session_log import SessionLog as SessionLog from .log_list_response import LogListResponse as LogListResponse -from .session_recording import SessionRecording as SessionRecording from .upload_create_params import UploadCreateParams as UploadCreateParams from .upload_create_response import UploadCreateResponse as UploadCreateResponse from .recording_retrieve_response import RecordingRetrieveResponse as RecordingRetrieveResponse diff --git a/src/browserbase/types/sessions/log_list_response.py b/src/browserbase/types/sessions/log_list_response.py index 2b325a8c..efd848ab 100644 --- a/src/browserbase/types/sessions/log_list_response.py +++ b/src/browserbase/types/sessions/log_list_response.py @@ -1,10 +1,50 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -from typing import List +from typing import Dict, List, Optional from typing_extensions import TypeAlias -from .session_log import SessionLog +from pydantic import Field as FieldInfo -__all__ = ["LogListResponse"] +from ..._models import BaseModel -LogListResponse: TypeAlias = List[SessionLog] +__all__ = ["LogListResponse", "LogListResponseItem", "LogListResponseItemRequest", "LogListResponseItemResponse"] + + +class LogListResponseItemRequest(BaseModel): + params: Dict[str, object] + + raw_body: str = FieldInfo(alias="rawBody") + + timestamp: Optional[int] = None + """milliseconds that have elapsed since the UNIX epoch""" + + +class LogListResponseItemResponse(BaseModel): + raw_body: str = FieldInfo(alias="rawBody") + + result: Dict[str, object] + + timestamp: Optional[int] = None + """milliseconds that have elapsed since the UNIX epoch""" + + +class LogListResponseItem(BaseModel): + method: str + + page_id: int = FieldInfo(alias="pageId") + + session_id: str = FieldInfo(alias="sessionId") + + frame_id: Optional[str] = FieldInfo(alias="frameId", default=None) + + loader_id: Optional[str] = FieldInfo(alias="loaderId", default=None) + + request: Optional[LogListResponseItemRequest] = None + + response: Optional[LogListResponseItemResponse] = None + + timestamp: Optional[int] = None + """milliseconds that have elapsed since the UNIX epoch""" + + +LogListResponse: TypeAlias = List[LogListResponseItem] diff --git a/src/browserbase/types/sessions/recording_retrieve_response.py b/src/browserbase/types/sessions/recording_retrieve_response.py index 951969bb..d3613b8c 100644 --- a/src/browserbase/types/sessions/recording_retrieve_response.py +++ b/src/browserbase/types/sessions/recording_retrieve_response.py @@ -1,10 +1,28 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -from typing import List +from typing import Dict, List from typing_extensions import TypeAlias -from .session_recording import SessionRecording +from pydantic import Field as FieldInfo -__all__ = ["RecordingRetrieveResponse"] +from ..._models import BaseModel -RecordingRetrieveResponse: TypeAlias = List[SessionRecording] +__all__ = ["RecordingRetrieveResponse", "RecordingRetrieveResponseItem"] + + +class RecordingRetrieveResponseItem(BaseModel): + data: Dict[str, object] + """ + See + [rrweb documentation](https://github.com/rrweb-io/rrweb/blob/master/docs/recipes/dive-into-event.md). + """ + + session_id: str = FieldInfo(alias="sessionId") + + timestamp: int + """milliseconds that have elapsed since the UNIX epoch""" + + type: int + + +RecordingRetrieveResponse: TypeAlias = List[RecordingRetrieveResponseItem] diff --git a/src/browserbase/types/sessions/session_log.py b/src/browserbase/types/sessions/session_log.py deleted file mode 100644 index 428f518a..00000000 --- a/src/browserbase/types/sessions/session_log.py +++ /dev/null @@ -1,46 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from typing import Dict, Optional - -from pydantic import Field as FieldInfo - -from ..._models import BaseModel - -__all__ = ["SessionLog", "Request", "Response"] - - -class Request(BaseModel): - params: Dict[str, object] - - raw_body: str = FieldInfo(alias="rawBody") - - timestamp: Optional[int] = None - """milliseconds that have elapsed since the UNIX epoch""" - - -class Response(BaseModel): - raw_body: str = FieldInfo(alias="rawBody") - - result: Dict[str, object] - - timestamp: Optional[int] = None - """milliseconds that have elapsed since the UNIX epoch""" - - -class SessionLog(BaseModel): - method: str - - page_id: int = FieldInfo(alias="pageId") - - session_id: str = FieldInfo(alias="sessionId") - - frame_id: Optional[str] = FieldInfo(alias="frameId", default=None) - - loader_id: Optional[str] = FieldInfo(alias="loaderId", default=None) - - request: Optional[Request] = None - - response: Optional[Response] = None - - timestamp: Optional[int] = None - """milliseconds that have elapsed since the UNIX epoch""" diff --git a/src/browserbase/types/sessions/session_recording.py b/src/browserbase/types/sessions/session_recording.py deleted file mode 100644 index c8471371..00000000 --- a/src/browserbase/types/sessions/session_recording.py +++ /dev/null @@ -1,24 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from typing import Dict - -from pydantic import Field as FieldInfo - -from ..._models import BaseModel - -__all__ = ["SessionRecording"] - - -class SessionRecording(BaseModel): - data: Dict[str, object] - """ - See - [rrweb documentation](https://github.com/rrweb-io/rrweb/blob/master/docs/recipes/dive-into-event.md). - """ - - session_id: str = FieldInfo(alias="sessionId") - - timestamp: int - """milliseconds that have elapsed since the UNIX epoch""" - - type: int diff --git a/tests/api_resources/test_contexts.py b/tests/api_resources/test_contexts.py index 31fb97d0..4ad27733 100644 --- a/tests/api_resources/test_contexts.py +++ b/tests/api_resources/test_contexts.py @@ -9,7 +9,11 @@ from browserbase import Browserbase, AsyncBrowserbase from tests.utils import assert_matches_type -from browserbase.types import Context, ContextCreateResponse, ContextUpdateResponse +from browserbase.types import ( + ContextCreateResponse, + ContextUpdateResponse, + ContextRetrieveResponse, +) base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") @@ -19,11 +23,6 @@ class TestContexts: @parametrize def test_method_create(self, client: Browserbase) -> None: - context = client.contexts.create() - assert_matches_type(ContextCreateResponse, context, path=["response"]) - - @parametrize - def test_method_create_with_all_params(self, client: Browserbase) -> None: context = client.contexts.create( project_id="projectId", ) @@ -31,7 +30,9 @@ def test_method_create_with_all_params(self, client: Browserbase) -> None: @parametrize def test_raw_response_create(self, client: Browserbase) -> None: - response = client.contexts.with_raw_response.create() + response = client.contexts.with_raw_response.create( + project_id="projectId", + ) assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -40,7 +41,9 @@ def test_raw_response_create(self, client: Browserbase) -> None: @parametrize def test_streaming_response_create(self, client: Browserbase) -> None: - with client.contexts.with_streaming_response.create() as response: + with client.contexts.with_streaming_response.create( + project_id="projectId", + ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -54,7 +57,7 @@ def test_method_retrieve(self, client: Browserbase) -> None: context = client.contexts.retrieve( "id", ) - assert_matches_type(Context, context, path=["response"]) + assert_matches_type(ContextRetrieveResponse, context, path=["response"]) @parametrize def test_raw_response_retrieve(self, client: Browserbase) -> None: @@ -65,7 +68,7 @@ def test_raw_response_retrieve(self, client: Browserbase) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" context = response.parse() - assert_matches_type(Context, context, path=["response"]) + assert_matches_type(ContextRetrieveResponse, context, path=["response"]) @parametrize def test_streaming_response_retrieve(self, client: Browserbase) -> None: @@ -76,7 +79,7 @@ def test_streaming_response_retrieve(self, client: Browserbase) -> None: assert response.http_request.headers.get("X-Stainless-Lang") == "python" context = response.parse() - assert_matches_type(Context, context, path=["response"]) + assert_matches_type(ContextRetrieveResponse, context, path=["response"]) assert cast(Any, response.is_closed) is True @@ -125,44 +128,6 @@ def test_path_params_update(self, client: Browserbase) -> None: "", ) - @parametrize - def test_method_delete(self, client: Browserbase) -> None: - context = client.contexts.delete( - "id", - ) - assert context is None - - @parametrize - def test_raw_response_delete(self, client: Browserbase) -> None: - response = client.contexts.with_raw_response.delete( - "id", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - context = response.parse() - assert context is None - - @parametrize - def test_streaming_response_delete(self, client: Browserbase) -> None: - with client.contexts.with_streaming_response.delete( - "id", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - context = response.parse() - assert context is None - - assert cast(Any, response.is_closed) is True - - @parametrize - def test_path_params_delete(self, client: Browserbase) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): - client.contexts.with_raw_response.delete( - "", - ) - class TestAsyncContexts: parametrize = pytest.mark.parametrize( @@ -171,11 +136,6 @@ class TestAsyncContexts: @parametrize async def test_method_create(self, async_client: AsyncBrowserbase) -> None: - context = await async_client.contexts.create() - assert_matches_type(ContextCreateResponse, context, path=["response"]) - - @parametrize - async def test_method_create_with_all_params(self, async_client: AsyncBrowserbase) -> None: context = await async_client.contexts.create( project_id="projectId", ) @@ -183,7 +143,9 @@ async def test_method_create_with_all_params(self, async_client: AsyncBrowserbas @parametrize async def test_raw_response_create(self, async_client: AsyncBrowserbase) -> None: - response = await async_client.contexts.with_raw_response.create() + response = await async_client.contexts.with_raw_response.create( + project_id="projectId", + ) assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -192,7 +154,9 @@ async def test_raw_response_create(self, async_client: AsyncBrowserbase) -> None @parametrize async def test_streaming_response_create(self, async_client: AsyncBrowserbase) -> None: - async with async_client.contexts.with_streaming_response.create() as response: + async with async_client.contexts.with_streaming_response.create( + project_id="projectId", + ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -206,7 +170,7 @@ async def test_method_retrieve(self, async_client: AsyncBrowserbase) -> None: context = await async_client.contexts.retrieve( "id", ) - assert_matches_type(Context, context, path=["response"]) + assert_matches_type(ContextRetrieveResponse, context, path=["response"]) @parametrize async def test_raw_response_retrieve(self, async_client: AsyncBrowserbase) -> None: @@ -217,7 +181,7 @@ async def test_raw_response_retrieve(self, async_client: AsyncBrowserbase) -> No assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" context = await response.parse() - assert_matches_type(Context, context, path=["response"]) + assert_matches_type(ContextRetrieveResponse, context, path=["response"]) @parametrize async def test_streaming_response_retrieve(self, async_client: AsyncBrowserbase) -> None: @@ -228,7 +192,7 @@ async def test_streaming_response_retrieve(self, async_client: AsyncBrowserbase) assert response.http_request.headers.get("X-Stainless-Lang") == "python" context = await response.parse() - assert_matches_type(Context, context, path=["response"]) + assert_matches_type(ContextRetrieveResponse, context, path=["response"]) assert cast(Any, response.is_closed) is True @@ -276,41 +240,3 @@ async def test_path_params_update(self, async_client: AsyncBrowserbase) -> None: await async_client.contexts.with_raw_response.update( "", ) - - @parametrize - async def test_method_delete(self, async_client: AsyncBrowserbase) -> None: - context = await async_client.contexts.delete( - "id", - ) - assert context is None - - @parametrize - async def test_raw_response_delete(self, async_client: AsyncBrowserbase) -> None: - response = await async_client.contexts.with_raw_response.delete( - "id", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - context = await response.parse() - assert context is None - - @parametrize - async def test_streaming_response_delete(self, async_client: AsyncBrowserbase) -> None: - async with async_client.contexts.with_streaming_response.delete( - "id", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - context = await response.parse() - assert context is None - - assert cast(Any, response.is_closed) is True - - @parametrize - async def test_path_params_delete(self, async_client: AsyncBrowserbase) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): - await async_client.contexts.with_raw_response.delete( - "", - ) diff --git a/tests/api_resources/test_extensions.py b/tests/api_resources/test_extensions.py index 6b6a0183..e32ae9b0 100644 --- a/tests/api_resources/test_extensions.py +++ b/tests/api_resources/test_extensions.py @@ -9,7 +9,7 @@ from browserbase import Browserbase, AsyncBrowserbase from tests.utils import assert_matches_type -from browserbase.types import Extension +from browserbase.types import ExtensionCreateResponse, ExtensionRetrieveResponse base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") @@ -22,7 +22,7 @@ def test_method_create(self, client: Browserbase) -> None: extension = client.extensions.create( file=b"raw file contents", ) - assert_matches_type(Extension, extension, path=["response"]) + assert_matches_type(ExtensionCreateResponse, extension, path=["response"]) @parametrize def test_raw_response_create(self, client: Browserbase) -> None: @@ -33,7 +33,7 @@ def test_raw_response_create(self, client: Browserbase) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" extension = response.parse() - assert_matches_type(Extension, extension, path=["response"]) + assert_matches_type(ExtensionCreateResponse, extension, path=["response"]) @parametrize def test_streaming_response_create(self, client: Browserbase) -> None: @@ -44,7 +44,7 @@ def test_streaming_response_create(self, client: Browserbase) -> None: assert response.http_request.headers.get("X-Stainless-Lang") == "python" extension = response.parse() - assert_matches_type(Extension, extension, path=["response"]) + assert_matches_type(ExtensionCreateResponse, extension, path=["response"]) assert cast(Any, response.is_closed) is True @@ -53,7 +53,7 @@ def test_method_retrieve(self, client: Browserbase) -> None: extension = client.extensions.retrieve( "id", ) - assert_matches_type(Extension, extension, path=["response"]) + assert_matches_type(ExtensionRetrieveResponse, extension, path=["response"]) @parametrize def test_raw_response_retrieve(self, client: Browserbase) -> None: @@ -64,7 +64,7 @@ def test_raw_response_retrieve(self, client: Browserbase) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" extension = response.parse() - assert_matches_type(Extension, extension, path=["response"]) + assert_matches_type(ExtensionRetrieveResponse, extension, path=["response"]) @parametrize def test_streaming_response_retrieve(self, client: Browserbase) -> None: @@ -75,7 +75,7 @@ def test_streaming_response_retrieve(self, client: Browserbase) -> None: assert response.http_request.headers.get("X-Stainless-Lang") == "python" extension = response.parse() - assert_matches_type(Extension, extension, path=["response"]) + assert_matches_type(ExtensionRetrieveResponse, extension, path=["response"]) assert cast(Any, response.is_closed) is True @@ -135,7 +135,7 @@ async def test_method_create(self, async_client: AsyncBrowserbase) -> None: extension = await async_client.extensions.create( file=b"raw file contents", ) - assert_matches_type(Extension, extension, path=["response"]) + assert_matches_type(ExtensionCreateResponse, extension, path=["response"]) @parametrize async def test_raw_response_create(self, async_client: AsyncBrowserbase) -> None: @@ -146,7 +146,7 @@ async def test_raw_response_create(self, async_client: AsyncBrowserbase) -> None assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" extension = await response.parse() - assert_matches_type(Extension, extension, path=["response"]) + assert_matches_type(ExtensionCreateResponse, extension, path=["response"]) @parametrize async def test_streaming_response_create(self, async_client: AsyncBrowserbase) -> None: @@ -157,7 +157,7 @@ async def test_streaming_response_create(self, async_client: AsyncBrowserbase) - assert response.http_request.headers.get("X-Stainless-Lang") == "python" extension = await response.parse() - assert_matches_type(Extension, extension, path=["response"]) + assert_matches_type(ExtensionCreateResponse, extension, path=["response"]) assert cast(Any, response.is_closed) is True @@ -166,7 +166,7 @@ async def test_method_retrieve(self, async_client: AsyncBrowserbase) -> None: extension = await async_client.extensions.retrieve( "id", ) - assert_matches_type(Extension, extension, path=["response"]) + assert_matches_type(ExtensionRetrieveResponse, extension, path=["response"]) @parametrize async def test_raw_response_retrieve(self, async_client: AsyncBrowserbase) -> None: @@ -177,7 +177,7 @@ async def test_raw_response_retrieve(self, async_client: AsyncBrowserbase) -> No assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" extension = await response.parse() - assert_matches_type(Extension, extension, path=["response"]) + assert_matches_type(ExtensionRetrieveResponse, extension, path=["response"]) @parametrize async def test_streaming_response_retrieve(self, async_client: AsyncBrowserbase) -> None: @@ -188,7 +188,7 @@ async def test_streaming_response_retrieve(self, async_client: AsyncBrowserbase) assert response.http_request.headers.get("X-Stainless-Lang") == "python" extension = await response.parse() - assert_matches_type(Extension, extension, path=["response"]) + assert_matches_type(ExtensionRetrieveResponse, extension, path=["response"]) assert cast(Any, response.is_closed) is True diff --git a/tests/api_resources/test_projects.py b/tests/api_resources/test_projects.py index c8241bf8..0d8e3c94 100644 --- a/tests/api_resources/test_projects.py +++ b/tests/api_resources/test_projects.py @@ -9,7 +9,7 @@ from browserbase import Browserbase, AsyncBrowserbase from tests.utils import assert_matches_type -from browserbase.types import Project, ProjectUsage, ProjectListResponse +from browserbase.types import ProjectListResponse, ProjectUsageResponse, ProjectRetrieveResponse base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") @@ -22,7 +22,7 @@ def test_method_retrieve(self, client: Browserbase) -> None: project = client.projects.retrieve( "id", ) - assert_matches_type(Project, project, path=["response"]) + assert_matches_type(ProjectRetrieveResponse, project, path=["response"]) @parametrize def test_raw_response_retrieve(self, client: Browserbase) -> None: @@ -33,7 +33,7 @@ def test_raw_response_retrieve(self, client: Browserbase) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" project = response.parse() - assert_matches_type(Project, project, path=["response"]) + assert_matches_type(ProjectRetrieveResponse, project, path=["response"]) @parametrize def test_streaming_response_retrieve(self, client: Browserbase) -> None: @@ -44,7 +44,7 @@ def test_streaming_response_retrieve(self, client: Browserbase) -> None: assert response.http_request.headers.get("X-Stainless-Lang") == "python" project = response.parse() - assert_matches_type(Project, project, path=["response"]) + assert_matches_type(ProjectRetrieveResponse, project, path=["response"]) assert cast(Any, response.is_closed) is True @@ -85,7 +85,7 @@ def test_method_usage(self, client: Browserbase) -> None: project = client.projects.usage( "id", ) - assert_matches_type(ProjectUsage, project, path=["response"]) + assert_matches_type(ProjectUsageResponse, project, path=["response"]) @parametrize def test_raw_response_usage(self, client: Browserbase) -> None: @@ -96,7 +96,7 @@ def test_raw_response_usage(self, client: Browserbase) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" project = response.parse() - assert_matches_type(ProjectUsage, project, path=["response"]) + assert_matches_type(ProjectUsageResponse, project, path=["response"]) @parametrize def test_streaming_response_usage(self, client: Browserbase) -> None: @@ -107,7 +107,7 @@ def test_streaming_response_usage(self, client: Browserbase) -> None: assert response.http_request.headers.get("X-Stainless-Lang") == "python" project = response.parse() - assert_matches_type(ProjectUsage, project, path=["response"]) + assert_matches_type(ProjectUsageResponse, project, path=["response"]) assert cast(Any, response.is_closed) is True @@ -129,7 +129,7 @@ async def test_method_retrieve(self, async_client: AsyncBrowserbase) -> None: project = await async_client.projects.retrieve( "id", ) - assert_matches_type(Project, project, path=["response"]) + assert_matches_type(ProjectRetrieveResponse, project, path=["response"]) @parametrize async def test_raw_response_retrieve(self, async_client: AsyncBrowserbase) -> None: @@ -140,7 +140,7 @@ async def test_raw_response_retrieve(self, async_client: AsyncBrowserbase) -> No assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" project = await response.parse() - assert_matches_type(Project, project, path=["response"]) + assert_matches_type(ProjectRetrieveResponse, project, path=["response"]) @parametrize async def test_streaming_response_retrieve(self, async_client: AsyncBrowserbase) -> None: @@ -151,7 +151,7 @@ async def test_streaming_response_retrieve(self, async_client: AsyncBrowserbase) assert response.http_request.headers.get("X-Stainless-Lang") == "python" project = await response.parse() - assert_matches_type(Project, project, path=["response"]) + assert_matches_type(ProjectRetrieveResponse, project, path=["response"]) assert cast(Any, response.is_closed) is True @@ -192,7 +192,7 @@ async def test_method_usage(self, async_client: AsyncBrowserbase) -> None: project = await async_client.projects.usage( "id", ) - assert_matches_type(ProjectUsage, project, path=["response"]) + assert_matches_type(ProjectUsageResponse, project, path=["response"]) @parametrize async def test_raw_response_usage(self, async_client: AsyncBrowserbase) -> None: @@ -203,7 +203,7 @@ async def test_raw_response_usage(self, async_client: AsyncBrowserbase) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" project = await response.parse() - assert_matches_type(ProjectUsage, project, path=["response"]) + assert_matches_type(ProjectUsageResponse, project, path=["response"]) @parametrize async def test_streaming_response_usage(self, async_client: AsyncBrowserbase) -> None: @@ -214,7 +214,7 @@ async def test_streaming_response_usage(self, async_client: AsyncBrowserbase) -> assert response.http_request.headers.get("X-Stainless-Lang") == "python" project = await response.parse() - assert_matches_type(ProjectUsage, project, path=["response"]) + assert_matches_type(ProjectUsageResponse, project, path=["response"]) assert cast(Any, response.is_closed) is True diff --git a/tests/api_resources/test_sessions.py b/tests/api_resources/test_sessions.py index 571daefd..7a16f64f 100644 --- a/tests/api_resources/test_sessions.py +++ b/tests/api_resources/test_sessions.py @@ -10,10 +10,10 @@ from browserbase import Browserbase, AsyncBrowserbase from tests.utils import assert_matches_type from browserbase.types import ( - Session, - SessionLiveURLs, SessionListResponse, + SessionDebugResponse, SessionCreateResponse, + SessionUpdateResponse, SessionRetrieveResponse, ) @@ -25,13 +25,15 @@ class TestSessions: @parametrize def test_method_create(self, client: Browserbase) -> None: - session = client.sessions.create() + session = client.sessions.create( + project_id="projectId", + ) assert_matches_type(SessionCreateResponse, session, path=["response"]) @parametrize def test_method_create_with_all_params(self, client: Browserbase) -> None: session = client.sessions.create( - api_timeout=60, + project_id="projectId", browser_settings={ "advanced_stealth": True, "block_ads": True, @@ -42,6 +44,19 @@ def test_method_create_with_all_params(self, client: Browserbase) -> None: "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, "os": "windows", "record_session": True, @@ -53,16 +68,28 @@ def test_method_create_with_all_params(self, client: Browserbase) -> None: }, extension_id="extensionId", keep_alive=True, - project_id="projectId", - proxies=True, + proxies=[ + { + "type": "browserbase", + "domain_pattern": "domainPattern", + "geolocation": { + "country": "xx", + "city": "city", + "state": "xx", + }, + } + ], region="us-west-2", + api_timeout=60, user_metadata={"foo": "bar"}, ) assert_matches_type(SessionCreateResponse, session, path=["response"]) @parametrize def test_raw_response_create(self, client: Browserbase) -> None: - response = client.sessions.with_raw_response.create() + response = client.sessions.with_raw_response.create( + project_id="projectId", + ) assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -71,7 +98,9 @@ def test_raw_response_create(self, client: Browserbase) -> None: @parametrize def test_streaming_response_create(self, client: Browserbase) -> None: - with client.sessions.with_streaming_response.create() as response: + with client.sessions.with_streaming_response.create( + project_id="projectId", + ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -122,42 +151,36 @@ def test_path_params_retrieve(self, client: Browserbase) -> None: def test_method_update(self, client: Browserbase) -> None: session = client.sessions.update( id="id", - status="REQUEST_RELEASE", - ) - assert_matches_type(Session, session, path=["response"]) - - @parametrize - def test_method_update_with_all_params(self, client: Browserbase) -> None: - session = client.sessions.update( - id="id", - status="REQUEST_RELEASE", project_id="projectId", + status="REQUEST_RELEASE", ) - assert_matches_type(Session, session, path=["response"]) + assert_matches_type(SessionUpdateResponse, session, path=["response"]) @parametrize def test_raw_response_update(self, client: Browserbase) -> None: response = client.sessions.with_raw_response.update( id="id", + project_id="projectId", status="REQUEST_RELEASE", ) assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" session = response.parse() - assert_matches_type(Session, session, path=["response"]) + assert_matches_type(SessionUpdateResponse, session, path=["response"]) @parametrize def test_streaming_response_update(self, client: Browserbase) -> None: with client.sessions.with_streaming_response.update( id="id", + project_id="projectId", status="REQUEST_RELEASE", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" session = response.parse() - assert_matches_type(Session, session, path=["response"]) + assert_matches_type(SessionUpdateResponse, session, path=["response"]) assert cast(Any, response.is_closed) is True @@ -166,6 +189,7 @@ def test_path_params_update(self, client: Browserbase) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): client.sessions.with_raw_response.update( id="", + project_id="projectId", status="REQUEST_RELEASE", ) @@ -207,7 +231,7 @@ def test_method_debug(self, client: Browserbase) -> None: session = client.sessions.debug( "id", ) - assert_matches_type(SessionLiveURLs, session, path=["response"]) + assert_matches_type(SessionDebugResponse, session, path=["response"]) @parametrize def test_raw_response_debug(self, client: Browserbase) -> None: @@ -218,7 +242,7 @@ def test_raw_response_debug(self, client: Browserbase) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" session = response.parse() - assert_matches_type(SessionLiveURLs, session, path=["response"]) + assert_matches_type(SessionDebugResponse, session, path=["response"]) @parametrize def test_streaming_response_debug(self, client: Browserbase) -> None: @@ -229,7 +253,7 @@ def test_streaming_response_debug(self, client: Browserbase) -> None: assert response.http_request.headers.get("X-Stainless-Lang") == "python" session = response.parse() - assert_matches_type(SessionLiveURLs, session, path=["response"]) + assert_matches_type(SessionDebugResponse, session, path=["response"]) assert cast(Any, response.is_closed) is True @@ -248,13 +272,15 @@ class TestAsyncSessions: @parametrize async def test_method_create(self, async_client: AsyncBrowserbase) -> None: - session = await async_client.sessions.create() + session = await async_client.sessions.create( + project_id="projectId", + ) assert_matches_type(SessionCreateResponse, session, path=["response"]) @parametrize async def test_method_create_with_all_params(self, async_client: AsyncBrowserbase) -> None: session = await async_client.sessions.create( - api_timeout=60, + project_id="projectId", browser_settings={ "advanced_stealth": True, "block_ads": True, @@ -265,6 +291,19 @@ async def test_method_create_with_all_params(self, async_client: AsyncBrowserbas "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, "os": "windows", "record_session": True, @@ -276,16 +315,28 @@ async def test_method_create_with_all_params(self, async_client: AsyncBrowserbas }, extension_id="extensionId", keep_alive=True, - project_id="projectId", - proxies=True, + proxies=[ + { + "type": "browserbase", + "domain_pattern": "domainPattern", + "geolocation": { + "country": "xx", + "city": "city", + "state": "xx", + }, + } + ], region="us-west-2", + api_timeout=60, user_metadata={"foo": "bar"}, ) assert_matches_type(SessionCreateResponse, session, path=["response"]) @parametrize async def test_raw_response_create(self, async_client: AsyncBrowserbase) -> None: - response = await async_client.sessions.with_raw_response.create() + response = await async_client.sessions.with_raw_response.create( + project_id="projectId", + ) assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -294,7 +345,9 @@ async def test_raw_response_create(self, async_client: AsyncBrowserbase) -> None @parametrize async def test_streaming_response_create(self, async_client: AsyncBrowserbase) -> None: - async with async_client.sessions.with_streaming_response.create() as response: + async with async_client.sessions.with_streaming_response.create( + project_id="projectId", + ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -345,42 +398,36 @@ async def test_path_params_retrieve(self, async_client: AsyncBrowserbase) -> Non async def test_method_update(self, async_client: AsyncBrowserbase) -> None: session = await async_client.sessions.update( id="id", - status="REQUEST_RELEASE", - ) - assert_matches_type(Session, session, path=["response"]) - - @parametrize - async def test_method_update_with_all_params(self, async_client: AsyncBrowserbase) -> None: - session = await async_client.sessions.update( - id="id", - status="REQUEST_RELEASE", project_id="projectId", + status="REQUEST_RELEASE", ) - assert_matches_type(Session, session, path=["response"]) + assert_matches_type(SessionUpdateResponse, session, path=["response"]) @parametrize async def test_raw_response_update(self, async_client: AsyncBrowserbase) -> None: response = await async_client.sessions.with_raw_response.update( id="id", + project_id="projectId", status="REQUEST_RELEASE", ) assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" session = await response.parse() - assert_matches_type(Session, session, path=["response"]) + assert_matches_type(SessionUpdateResponse, session, path=["response"]) @parametrize async def test_streaming_response_update(self, async_client: AsyncBrowserbase) -> None: async with async_client.sessions.with_streaming_response.update( id="id", + project_id="projectId", status="REQUEST_RELEASE", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" session = await response.parse() - assert_matches_type(Session, session, path=["response"]) + assert_matches_type(SessionUpdateResponse, session, path=["response"]) assert cast(Any, response.is_closed) is True @@ -389,6 +436,7 @@ async def test_path_params_update(self, async_client: AsyncBrowserbase) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): await async_client.sessions.with_raw_response.update( id="", + project_id="projectId", status="REQUEST_RELEASE", ) @@ -430,7 +478,7 @@ async def test_method_debug(self, async_client: AsyncBrowserbase) -> None: session = await async_client.sessions.debug( "id", ) - assert_matches_type(SessionLiveURLs, session, path=["response"]) + assert_matches_type(SessionDebugResponse, session, path=["response"]) @parametrize async def test_raw_response_debug(self, async_client: AsyncBrowserbase) -> None: @@ -441,7 +489,7 @@ async def test_raw_response_debug(self, async_client: AsyncBrowserbase) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" session = await response.parse() - assert_matches_type(SessionLiveURLs, session, path=["response"]) + assert_matches_type(SessionDebugResponse, session, path=["response"]) @parametrize async def test_streaming_response_debug(self, async_client: AsyncBrowserbase) -> None: @@ -452,7 +500,7 @@ async def test_streaming_response_debug(self, async_client: AsyncBrowserbase) -> assert response.http_request.headers.get("X-Stainless-Lang") == "python" session = await response.parse() - assert_matches_type(SessionLiveURLs, session, path=["response"]) + assert_matches_type(SessionDebugResponse, session, path=["response"]) assert cast(Any, response.is_closed) is True diff --git a/tests/test_client.py b/tests/test_client.py index 71396d22..608cc1f9 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -864,7 +864,7 @@ def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter, clien respx_mock.post("/v1/sessions").mock(side_effect=httpx.TimeoutException("Test timeout error")) with pytest.raises(APITimeoutError): - client.sessions.with_streaming_response.create().__enter__() + client.sessions.with_streaming_response.create(project_id="projectId").__enter__() assert _get_open_connections(client) == 0 @@ -874,7 +874,7 @@ def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter, client respx_mock.post("/v1/sessions").mock(return_value=httpx.Response(500)) with pytest.raises(APIStatusError): - client.sessions.with_streaming_response.create().__enter__() + client.sessions.with_streaming_response.create(project_id="projectId").__enter__() assert _get_open_connections(client) == 0 @pytest.mark.parametrize("failures_before_success", [0, 2, 4]) @@ -903,7 +903,7 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: respx_mock.post("/v1/sessions").mock(side_effect=retry_handler) - response = client.sessions.with_raw_response.create() + response = client.sessions.with_raw_response.create(project_id="projectId") assert response.retries_taken == failures_before_success assert int(response.http_request.headers.get("x-stainless-retry-count")) == failures_before_success @@ -927,7 +927,9 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: respx_mock.post("/v1/sessions").mock(side_effect=retry_handler) - response = client.sessions.with_raw_response.create(extra_headers={"x-stainless-retry-count": Omit()}) + response = client.sessions.with_raw_response.create( + project_id="projectId", extra_headers={"x-stainless-retry-count": Omit()} + ) assert len(response.http_request.headers.get_list("x-stainless-retry-count")) == 0 @@ -950,7 +952,9 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: respx_mock.post("/v1/sessions").mock(side_effect=retry_handler) - response = client.sessions.with_raw_response.create(extra_headers={"x-stainless-retry-count": "42"}) + response = client.sessions.with_raw_response.create( + project_id="projectId", extra_headers={"x-stainless-retry-count": "42"} + ) assert response.http_request.headers.get("x-stainless-retry-count") == "42" @@ -1766,7 +1770,7 @@ async def test_retrying_timeout_errors_doesnt_leak( respx_mock.post("/v1/sessions").mock(side_effect=httpx.TimeoutException("Test timeout error")) with pytest.raises(APITimeoutError): - await async_client.sessions.with_streaming_response.create().__aenter__() + await async_client.sessions.with_streaming_response.create(project_id="projectId").__aenter__() assert _get_open_connections(async_client) == 0 @@ -1778,7 +1782,7 @@ async def test_retrying_status_errors_doesnt_leak( respx_mock.post("/v1/sessions").mock(return_value=httpx.Response(500)) with pytest.raises(APIStatusError): - await async_client.sessions.with_streaming_response.create().__aenter__() + await async_client.sessions.with_streaming_response.create(project_id="projectId").__aenter__() assert _get_open_connections(async_client) == 0 @pytest.mark.parametrize("failures_before_success", [0, 2, 4]) @@ -1807,7 +1811,7 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: respx_mock.post("/v1/sessions").mock(side_effect=retry_handler) - response = await client.sessions.with_raw_response.create() + response = await client.sessions.with_raw_response.create(project_id="projectId") assert response.retries_taken == failures_before_success assert int(response.http_request.headers.get("x-stainless-retry-count")) == failures_before_success @@ -1831,7 +1835,9 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: respx_mock.post("/v1/sessions").mock(side_effect=retry_handler) - response = await client.sessions.with_raw_response.create(extra_headers={"x-stainless-retry-count": Omit()}) + response = await client.sessions.with_raw_response.create( + project_id="projectId", extra_headers={"x-stainless-retry-count": Omit()} + ) assert len(response.http_request.headers.get_list("x-stainless-retry-count")) == 0 @@ -1854,7 +1860,9 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: respx_mock.post("/v1/sessions").mock(side_effect=retry_handler) - response = await client.sessions.with_raw_response.create(extra_headers={"x-stainless-retry-count": "42"}) + response = await client.sessions.with_raw_response.create( + project_id="projectId", extra_headers={"x-stainless-retry-count": "42"} + ) assert response.http_request.headers.get("x-stainless-retry-count") == "42" From 2ae68a36b670c1fd3ed67ac0ab5da187cd095fc3 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 24 Feb 2026 10:13:40 +0000 Subject: [PATCH 249/330] chore(internal): add request options to SSE classes --- src/browserbase/_response.py | 3 +++ src/browserbase/_streaming.py | 11 ++++++++--- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/browserbase/_response.py b/src/browserbase/_response.py index 5f8d0f48..eeef6426 100644 --- a/src/browserbase/_response.py +++ b/src/browserbase/_response.py @@ -152,6 +152,7 @@ def _parse(self, *, to: type[_T] | None = None) -> R | _T: ), response=self.http_response, client=cast(Any, self._client), + options=self._options, ), ) @@ -162,6 +163,7 @@ def _parse(self, *, to: type[_T] | None = None) -> R | _T: cast_to=extract_stream_chunk_type(self._stream_cls), response=self.http_response, client=cast(Any, self._client), + options=self._options, ), ) @@ -175,6 +177,7 @@ def _parse(self, *, to: type[_T] | None = None) -> R | _T: cast_to=cast_to, response=self.http_response, client=cast(Any, self._client), + options=self._options, ), ) diff --git a/src/browserbase/_streaming.py b/src/browserbase/_streaming.py index d107619d..d1ebde6c 100644 --- a/src/browserbase/_streaming.py +++ b/src/browserbase/_streaming.py @@ -4,7 +4,7 @@ import json import inspect from types import TracebackType -from typing import TYPE_CHECKING, Any, Generic, TypeVar, Iterator, AsyncIterator, cast +from typing import TYPE_CHECKING, Any, Generic, TypeVar, Iterator, Optional, AsyncIterator, cast from typing_extensions import Self, Protocol, TypeGuard, override, get_origin, runtime_checkable import httpx @@ -13,6 +13,7 @@ if TYPE_CHECKING: from ._client import Browserbase, AsyncBrowserbase + from ._models import FinalRequestOptions _T = TypeVar("_T") @@ -22,7 +23,7 @@ class Stream(Generic[_T]): """Provides the core interface to iterate over a synchronous stream response.""" response: httpx.Response - + _options: Optional[FinalRequestOptions] = None _decoder: SSEBytesDecoder def __init__( @@ -31,10 +32,12 @@ def __init__( cast_to: type[_T], response: httpx.Response, client: Browserbase, + options: Optional[FinalRequestOptions] = None, ) -> None: self.response = response self._cast_to = cast_to self._client = client + self._options = options self._decoder = client._make_sse_decoder() self._iterator = self.__stream__() @@ -85,7 +88,7 @@ class AsyncStream(Generic[_T]): """Provides the core interface to iterate over an asynchronous stream response.""" response: httpx.Response - + _options: Optional[FinalRequestOptions] = None _decoder: SSEDecoder | SSEBytesDecoder def __init__( @@ -94,10 +97,12 @@ def __init__( cast_to: type[_T], response: httpx.Response, client: AsyncBrowserbase, + options: Optional[FinalRequestOptions] = None, ) -> None: self.response = response self._cast_to = cast_to self._client = client + self._options = options self._decoder = client._make_sse_decoder() self._iterator = self.__stream__() From 795ea51f0f5378962413fd2c746b9f6ba1106a4d Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 24 Feb 2026 10:26:26 +0000 Subject: [PATCH 250/330] chore(internal): make `test_proxy_environment_variables` more resilient --- tests/test_client.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/test_client.py b/tests/test_client.py index 608cc1f9..15eebfc1 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -961,6 +961,8 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: 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") + # Delete in case our environment has this set + monkeypatch.delenv("HTTP_PROXY", raising=False) client = DefaultHttpxClient() @@ -1873,6 +1875,8 @@ async def test_get_platform(self) -> None: 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") + # Delete in case our environment has this set + monkeypatch.delenv("HTTP_PROXY", raising=False) client = DefaultAsyncHttpxClient() From c36960f8275b60d77708b0e4cc8ffa4a6866fed5 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 24 Feb 2026 16:49:52 +0000 Subject: [PATCH 251/330] feat(api): api update --- .stats.yml | 4 +- api.md | 1 + src/browserbase/resources/contexts.py | 82 ++++++++++++++++++++++++++- tests/api_resources/test_contexts.py | 76 +++++++++++++++++++++++++ 4 files changed, 160 insertions(+), 3 deletions(-) diff --git a/.stats.yml b/.stats.yml index b000c8c1..629b9d76 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ -configured_endpoints: 18 +configured_endpoints: 19 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fbrowserbase-be7a4aeebb1605262935b4b3ab446a95b1fad8a7d18098943dd548c8a486ef13.yml openapi_spec_hash: 1c950a109f80140711e7ae2cf87fddad -config_hash: b3ca4ec5b02e5333af51ebc2e9fdef1b +config_hash: b01d72cbe03bd762a73b05744086b2ec diff --git a/api.md b/api.md index 01454851..7e8a3f13 100644 --- a/api.md +++ b/api.md @@ -11,6 +11,7 @@ Methods: - client.contexts.create(\*\*params) -> ContextCreateResponse - client.contexts.retrieve(id) -> ContextRetrieveResponse - client.contexts.update(id) -> ContextUpdateResponse +- client.contexts.delete(id) -> None # Extensions diff --git a/src/browserbase/resources/contexts.py b/src/browserbase/resources/contexts.py index d2bb4167..1527af05 100644 --- a/src/browserbase/resources/contexts.py +++ b/src/browserbase/resources/contexts.py @@ -5,7 +5,7 @@ import httpx from ..types import context_create_params -from .._types import Body, Query, Headers, NotGiven, not_given +from .._types import Body, Query, Headers, NoneType, NotGiven, not_given from .._utils import maybe_transform, async_maybe_transform from .._compat import cached_property from .._resource import SyncAPIResource, AsyncAPIResource @@ -145,6 +145,40 @@ def update( cast_to=ContextUpdateResponse, ) + def delete( + self, + 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, + ) -> None: + """ + Delete a Context + + 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 id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + extra_headers = {"Accept": "*/*", **(extra_headers or {})} + return self._delete( + f"/v1/contexts/{id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=NoneType, + ) + class AsyncContextsResource(AsyncAPIResource): @cached_property @@ -268,6 +302,40 @@ async def update( cast_to=ContextUpdateResponse, ) + async def delete( + self, + 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, + ) -> None: + """ + Delete a Context + + 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 id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + extra_headers = {"Accept": "*/*", **(extra_headers or {})} + return await self._delete( + f"/v1/contexts/{id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=NoneType, + ) + class ContextsResourceWithRawResponse: def __init__(self, contexts: ContextsResource) -> None: @@ -282,6 +350,9 @@ def __init__(self, contexts: ContextsResource) -> None: self.update = to_raw_response_wrapper( contexts.update, ) + self.delete = to_raw_response_wrapper( + contexts.delete, + ) class AsyncContextsResourceWithRawResponse: @@ -297,6 +368,9 @@ def __init__(self, contexts: AsyncContextsResource) -> None: self.update = async_to_raw_response_wrapper( contexts.update, ) + self.delete = async_to_raw_response_wrapper( + contexts.delete, + ) class ContextsResourceWithStreamingResponse: @@ -312,6 +386,9 @@ def __init__(self, contexts: ContextsResource) -> None: self.update = to_streamed_response_wrapper( contexts.update, ) + self.delete = to_streamed_response_wrapper( + contexts.delete, + ) class AsyncContextsResourceWithStreamingResponse: @@ -327,3 +404,6 @@ def __init__(self, contexts: AsyncContextsResource) -> None: self.update = async_to_streamed_response_wrapper( contexts.update, ) + self.delete = async_to_streamed_response_wrapper( + contexts.delete, + ) diff --git a/tests/api_resources/test_contexts.py b/tests/api_resources/test_contexts.py index 4ad27733..d32ebc4b 100644 --- a/tests/api_resources/test_contexts.py +++ b/tests/api_resources/test_contexts.py @@ -128,6 +128,44 @@ def test_path_params_update(self, client: Browserbase) -> None: "", ) + @parametrize + def test_method_delete(self, client: Browserbase) -> None: + context = client.contexts.delete( + "id", + ) + assert context is None + + @parametrize + def test_raw_response_delete(self, client: Browserbase) -> None: + response = client.contexts.with_raw_response.delete( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + context = response.parse() + assert context is None + + @parametrize + def test_streaming_response_delete(self, client: Browserbase) -> None: + with client.contexts.with_streaming_response.delete( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + context = response.parse() + assert context is None + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_delete(self, client: Browserbase) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.contexts.with_raw_response.delete( + "", + ) + class TestAsyncContexts: parametrize = pytest.mark.parametrize( @@ -240,3 +278,41 @@ async def test_path_params_update(self, async_client: AsyncBrowserbase) -> None: await async_client.contexts.with_raw_response.update( "", ) + + @parametrize + async def test_method_delete(self, async_client: AsyncBrowserbase) -> None: + context = await async_client.contexts.delete( + "id", + ) + assert context is None + + @parametrize + async def test_raw_response_delete(self, async_client: AsyncBrowserbase) -> None: + response = await async_client.contexts.with_raw_response.delete( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + context = await response.parse() + assert context is None + + @parametrize + async def test_streaming_response_delete(self, async_client: AsyncBrowserbase) -> None: + async with async_client.contexts.with_streaming_response.delete( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + context = await response.parse() + assert context is None + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_delete(self, async_client: AsyncBrowserbase) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.contexts.with_raw_response.delete( + "", + ) From 80ecce5582ffb1c34ecfd5a74b8b79d9eca5ea36 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 25 Feb 2026 10:12:53 +0000 Subject: [PATCH 252/330] chore(internal): make `test_proxy_environment_variables` more resilient to env --- tests/test_client.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/tests/test_client.py b/tests/test_client.py index 15eebfc1..1f33c763 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -961,8 +961,14 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: 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") - # Delete in case our environment has this set + # Delete in case our environment has any proxy env vars set monkeypatch.delenv("HTTP_PROXY", raising=False) + monkeypatch.delenv("ALL_PROXY", raising=False) + monkeypatch.delenv("NO_PROXY", raising=False) + monkeypatch.delenv("http_proxy", raising=False) + monkeypatch.delenv("https_proxy", raising=False) + monkeypatch.delenv("all_proxy", raising=False) + monkeypatch.delenv("no_proxy", raising=False) client = DefaultHttpxClient() @@ -1875,8 +1881,14 @@ async def test_get_platform(self) -> None: 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") - # Delete in case our environment has this set + # Delete in case our environment has any proxy env vars set monkeypatch.delenv("HTTP_PROXY", raising=False) + monkeypatch.delenv("ALL_PROXY", raising=False) + monkeypatch.delenv("NO_PROXY", raising=False) + monkeypatch.delenv("http_proxy", raising=False) + monkeypatch.delenv("https_proxy", raising=False) + monkeypatch.delenv("all_proxy", raising=False) + monkeypatch.delenv("no_proxy", raising=False) client = DefaultAsyncHttpxClient() From 94a823ed8b8bc81fa9914406c60b6bcc4926345b Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 26 Feb 2026 00:39:19 +0000 Subject: [PATCH 253/330] feat(api): api update --- .stats.yml | 4 +- README.md | 1 - src/browserbase/resources/contexts.py | 12 +-- .../resources/sessions/sessions.py | 62 +++++++------- .../types/context_create_params.py | 7 +- .../types/session_create_params.py | 70 ++++++---------- .../types/session_create_response.py | 6 -- .../types/session_list_response.py | 6 -- .../types/session_retrieve_response.py | 6 -- .../types/session_update_params.py | 13 +-- .../types/session_update_response.py | 6 -- tests/api_resources/test_contexts.py | 26 +++--- tests/api_resources/test_sessions.py | 80 ++++++------------- tests/test_client.py | 28 +++---- 14 files changed, 125 insertions(+), 202 deletions(-) diff --git a/.stats.yml b/.stats.yml index 629b9d76..a0691ecb 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 19 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fbrowserbase-be7a4aeebb1605262935b4b3ab446a95b1fad8a7d18098943dd548c8a486ef13.yml -openapi_spec_hash: 1c950a109f80140711e7ae2cf87fddad +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fbrowserbase-a2379f6bf614a1efd1bbb22b2191bf1a3daf09fd42267c8c54ce4284392d1ea4.yml +openapi_spec_hash: 918f5ba73e08f044cfb77de95a8b7524 config_hash: b01d72cbe03bd762a73b05744086b2ec diff --git a/README.md b/README.md index dcb4b768..5d384354 100644 --- a/README.md +++ b/README.md @@ -122,7 +122,6 @@ from browserbase import Browserbase client = Browserbase() session = client.sessions.create( - project_id="projectId", browser_settings={}, ) print(session.browser_settings) diff --git a/src/browserbase/resources/contexts.py b/src/browserbase/resources/contexts.py index 1527af05..989fd48b 100644 --- a/src/browserbase/resources/contexts.py +++ b/src/browserbase/resources/contexts.py @@ -5,7 +5,7 @@ import httpx from ..types import context_create_params -from .._types import Body, Query, Headers, NoneType, NotGiven, not_given +from .._types import Body, Omit, Query, Headers, NoneType, NotGiven, omit, not_given from .._utils import maybe_transform, async_maybe_transform from .._compat import cached_property from .._resource import SyncAPIResource, AsyncAPIResource @@ -46,7 +46,7 @@ def with_streaming_response(self) -> ContextsResourceWithStreamingResponse: def create( self, *, - project_id: str, + project_id: str | 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, @@ -60,7 +60,8 @@ def create( project_id: The Project ID. Can be found in - [Settings](https://www.browserbase.com/settings). + [Settings](https://www.browserbase.com/settings). Optional - if not provided, + the project will be inferred from the API key. extra_headers: Send extra headers @@ -203,7 +204,7 @@ def with_streaming_response(self) -> AsyncContextsResourceWithStreamingResponse: async def create( self, *, - project_id: str, + project_id: str | 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, @@ -217,7 +218,8 @@ async def create( project_id: The Project ID. Can be found in - [Settings](https://www.browserbase.com/settings). + [Settings](https://www.browserbase.com/settings). Optional - if not provided, + the project will be inferred from the API key. extra_headers: Send extra headers diff --git a/src/browserbase/resources/sessions/sessions.py b/src/browserbase/resources/sessions/sessions.py index ceaaeb81..e7a28510 100644 --- a/src/browserbase/resources/sessions/sessions.py +++ b/src/browserbase/resources/sessions/sessions.py @@ -99,10 +99,10 @@ def with_streaming_response(self) -> SessionsResourceWithStreamingResponse: def create( self, *, - project_id: str, browser_settings: session_create_params.BrowserSettings | Omit = omit, extension_id: str | Omit = omit, keep_alive: bool | Omit = omit, + project_id: str | Omit = omit, proxies: Union[Iterable[session_create_params.ProxiesUnionMember0], bool] | Omit = omit, region: Literal["us-west-2", "us-east-1", "eu-central-1", "ap-southeast-1"] | Omit = omit, api_timeout: int | Omit = omit, @@ -117,17 +117,18 @@ def create( """Create a Session Args: - project_id: The Project ID. + extension_id: The uploaded Extension ID. - Can be found in - [Settings](https://www.browserbase.com/settings). - - extension_id: The uploaded Extension ID. See + See [Upload Extension](/reference/api/upload-an-extension). keep_alive: Set to true to keep the session alive even after disconnections. Available on the Hobby Plan and above. + project_id: The Project ID. Can be found in + [Settings](https://www.browserbase.com/settings). Optional - if not provided, + the project will be inferred from the API key. + proxies: Proxy configuration. Can be true for default proxy, or an array of proxy configurations. @@ -151,10 +152,10 @@ def create( "/v1/sessions", body=maybe_transform( { - "project_id": project_id, "browser_settings": browser_settings, "extension_id": extension_id, "keep_alive": keep_alive, + "project_id": project_id, "proxies": proxies, "region": region, "api_timeout": api_timeout, @@ -205,8 +206,8 @@ def update( self, id: str, *, - project_id: str, status: Literal["REQUEST_RELEASE"], + project_id: str | 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, @@ -214,17 +215,17 @@ def update( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> SessionUpdateResponse: - """Update a Session + """ + Update a Session Args: - project_id: The Project ID. - - Can be found in - [Settings](https://www.browserbase.com/settings). - status: Set to `REQUEST_RELEASE` to request that the session complete. Use before session's timeout to avoid additional charges. + project_id: The Project ID. Can be found in + [Settings](https://www.browserbase.com/settings). Optional - if not provided, + the project will be inferred from the API key. + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -239,8 +240,8 @@ def update( f"/v1/sessions/{id}", body=maybe_transform( { - "project_id": project_id, "status": status, + "project_id": project_id, }, session_update_params.SessionUpdateParams, ), @@ -370,10 +371,10 @@ def with_streaming_response(self) -> AsyncSessionsResourceWithStreamingResponse: async def create( self, *, - project_id: str, browser_settings: session_create_params.BrowserSettings | Omit = omit, extension_id: str | Omit = omit, keep_alive: bool | Omit = omit, + project_id: str | Omit = omit, proxies: Union[Iterable[session_create_params.ProxiesUnionMember0], bool] | Omit = omit, region: Literal["us-west-2", "us-east-1", "eu-central-1", "ap-southeast-1"] | Omit = omit, api_timeout: int | Omit = omit, @@ -388,17 +389,18 @@ async def create( """Create a Session Args: - project_id: The Project ID. + extension_id: The uploaded Extension ID. - Can be found in - [Settings](https://www.browserbase.com/settings). - - extension_id: The uploaded Extension ID. See + See [Upload Extension](/reference/api/upload-an-extension). keep_alive: Set to true to keep the session alive even after disconnections. Available on the Hobby Plan and above. + project_id: The Project ID. Can be found in + [Settings](https://www.browserbase.com/settings). Optional - if not provided, + the project will be inferred from the API key. + proxies: Proxy configuration. Can be true for default proxy, or an array of proxy configurations. @@ -422,10 +424,10 @@ async def create( "/v1/sessions", body=await async_maybe_transform( { - "project_id": project_id, "browser_settings": browser_settings, "extension_id": extension_id, "keep_alive": keep_alive, + "project_id": project_id, "proxies": proxies, "region": region, "api_timeout": api_timeout, @@ -476,8 +478,8 @@ async def update( self, id: str, *, - project_id: str, status: Literal["REQUEST_RELEASE"], + project_id: str | 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, @@ -485,17 +487,17 @@ async def update( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> SessionUpdateResponse: - """Update a Session + """ + Update a Session Args: - project_id: The Project ID. - - Can be found in - [Settings](https://www.browserbase.com/settings). - status: Set to `REQUEST_RELEASE` to request that the session complete. Use before session's timeout to avoid additional charges. + project_id: The Project ID. Can be found in + [Settings](https://www.browserbase.com/settings). Optional - if not provided, + the project will be inferred from the API key. + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -510,8 +512,8 @@ async def update( f"/v1/sessions/{id}", body=await async_maybe_transform( { - "project_id": project_id, "status": status, + "project_id": project_id, }, session_update_params.SessionUpdateParams, ), diff --git a/src/browserbase/types/context_create_params.py b/src/browserbase/types/context_create_params.py index 75cd1fcd..7f422f2d 100644 --- a/src/browserbase/types/context_create_params.py +++ b/src/browserbase/types/context_create_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,8 +10,9 @@ class ContextCreateParams(TypedDict, total=False): - project_id: Required[Annotated[str, PropertyInfo(alias="projectId")]] + project_id: Annotated[str, PropertyInfo(alias="projectId")] """The Project ID. - Can be found in [Settings](https://www.browserbase.com/settings). + Can be found in [Settings](https://www.browserbase.com/settings). Optional - if + not provided, the project will be inferred from the API key. """ diff --git a/src/browserbase/types/session_create_params.py b/src/browserbase/types/session_create_params.py index 2ba36400..17629fe2 100644 --- a/src/browserbase/types/session_create_params.py +++ b/src/browserbase/types/session_create_params.py @@ -2,33 +2,25 @@ from __future__ import annotations -from typing import Dict, List, Union, Iterable +from typing import Dict, Union, Iterable from typing_extensions import Literal, Required, Annotated, TypeAlias, TypedDict -from .._types import SequenceNotStr from .._utils import PropertyInfo __all__ = [ "SessionCreateParams", "BrowserSettings", "BrowserSettingsContext", - "BrowserSettingsFingerprint", - "BrowserSettingsFingerprintScreen", "BrowserSettingsViewport", "ProxiesUnionMember0", "ProxiesUnionMember0UnionMember0", "ProxiesUnionMember0UnionMember0Geolocation", "ProxiesUnionMember0UnionMember1", + "ProxiesUnionMember0UnionMember2", ] class SessionCreateParams(TypedDict, total=False): - project_id: Required[Annotated[str, PropertyInfo(alias="projectId")]] - """The Project ID. - - Can be found in [Settings](https://www.browserbase.com/settings). - """ - browser_settings: Annotated[BrowserSettings, PropertyInfo(alias="browserSettings")] extension_id: Annotated[str, PropertyInfo(alias="extensionId")] @@ -43,6 +35,13 @@ class SessionCreateParams(TypedDict, total=False): Available on the Hobby Plan and above. """ + project_id: Annotated[str, PropertyInfo(alias="projectId")] + """The Project ID. + + Can be found in [Settings](https://www.browserbase.com/settings). Optional - if + not provided, the project will be inferred from the API key. + """ + proxies: Union[Iterable[ProxiesUnionMember0], bool] """Proxy configuration. @@ -74,36 +73,6 @@ class BrowserSettingsContext(TypedDict, total=False): """Whether or not to persist the context after browsing. Defaults to `false`.""" -class BrowserSettingsFingerprintScreen(TypedDict, total=False): - max_height: Annotated[int, PropertyInfo(alias="maxHeight")] - - max_width: Annotated[int, PropertyInfo(alias="maxWidth")] - - min_height: Annotated[int, PropertyInfo(alias="minHeight")] - - min_width: Annotated[int, PropertyInfo(alias="minWidth")] - - -class BrowserSettingsFingerprint(TypedDict, total=False): - """ - See usage examples [on the Stealth Mode page](/features/stealth-mode#fingerprinting) - """ - - 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: BrowserSettingsFingerprintScreen - - class BrowserSettingsViewport(TypedDict, total=False): height: int """The height of the browser.""" @@ -139,12 +108,6 @@ class BrowserSettings(TypedDict, total=False): See [Upload Extension](/reference/api/upload-an-extension). """ - fingerprint: BrowserSettingsFingerprint - """ - See usage examples - [on the Stealth Mode page](/features/stealth-mode#fingerprinting) - """ - log_session: Annotated[bool, PropertyInfo(alias="logSession")] """Enable or disable session logging. Defaults to `true`.""" @@ -213,4 +176,17 @@ class ProxiesUnionMember0UnionMember1(TypedDict, total=False): """Username for external proxy authentication. Optional.""" -ProxiesUnionMember0: TypeAlias = Union[ProxiesUnionMember0UnionMember0, ProxiesUnionMember0UnionMember1] +class ProxiesUnionMember0UnionMember2(TypedDict, total=False): + type: Required[Literal["none"]] + """Type of proxy. Always 'none' for this config.""" + + domain_pattern: Annotated[str, PropertyInfo(alias="domainPattern")] + """Domain pattern for which this proxy should be used. + + If omitted, defaults to all domains. Optional. + """ + + +ProxiesUnionMember0: TypeAlias = Union[ + ProxiesUnionMember0UnionMember0, ProxiesUnionMember0UnionMember1, ProxiesUnionMember0UnionMember2 +] diff --git a/src/browserbase/types/session_create_response.py b/src/browserbase/types/session_create_response.py index b548d50f..7a863d14 100644 --- a/src/browserbase/types/session_create_response.py +++ b/src/browserbase/types/session_create_response.py @@ -45,17 +45,11 @@ class SessionCreateResponse(BaseModel): updated_at: datetime = FieldInfo(alias="updatedAt") - avg_cpu_usage: Optional[int] = FieldInfo(alias="avgCpuUsage", default=None) - """CPU used by the Session""" - context_id: Optional[str] = FieldInfo(alias="contextId", default=None) """Optional. The Context linked to the Session.""" ended_at: Optional[datetime] = FieldInfo(alias="endedAt", default=None) - memory_usage: Optional[int] = FieldInfo(alias="memoryUsage", default=None) - """Memory used by the Session""" - user_metadata: Optional[Dict[str, object]] = FieldInfo(alias="userMetadata", default=None) """Arbitrary user metadata to attach to the session. diff --git a/src/browserbase/types/session_list_response.py b/src/browserbase/types/session_list_response.py index 4c1bd885..103c2147 100644 --- a/src/browserbase/types/session_list_response.py +++ b/src/browserbase/types/session_list_response.py @@ -36,17 +36,11 @@ class SessionListResponseItem(BaseModel): updated_at: datetime = FieldInfo(alias="updatedAt") - avg_cpu_usage: Optional[int] = FieldInfo(alias="avgCpuUsage", default=None) - """CPU used by the Session""" - context_id: Optional[str] = FieldInfo(alias="contextId", default=None) """Optional. The Context linked to the Session.""" ended_at: Optional[datetime] = FieldInfo(alias="endedAt", default=None) - memory_usage: Optional[int] = FieldInfo(alias="memoryUsage", default=None) - """Memory used by the Session""" - user_metadata: Optional[Dict[str, object]] = FieldInfo(alias="userMetadata", default=None) """Arbitrary user metadata to attach to the session. diff --git a/src/browserbase/types/session_retrieve_response.py b/src/browserbase/types/session_retrieve_response.py index a9a4ff28..885c281e 100644 --- a/src/browserbase/types/session_retrieve_response.py +++ b/src/browserbase/types/session_retrieve_response.py @@ -36,9 +36,6 @@ class SessionRetrieveResponse(BaseModel): updated_at: datetime = FieldInfo(alias="updatedAt") - avg_cpu_usage: Optional[int] = FieldInfo(alias="avgCpuUsage", default=None) - """CPU used by the Session""" - connect_url: Optional[str] = FieldInfo(alias="connectUrl", default=None) """WebSocket URL to connect to the Session.""" @@ -47,9 +44,6 @@ class SessionRetrieveResponse(BaseModel): ended_at: Optional[datetime] = FieldInfo(alias="endedAt", default=None) - memory_usage: Optional[int] = FieldInfo(alias="memoryUsage", default=None) - """Memory used by the Session""" - selenium_remote_url: Optional[str] = FieldInfo(alias="seleniumRemoteUrl", default=None) """HTTP URL to connect to the Session.""" diff --git a/src/browserbase/types/session_update_params.py b/src/browserbase/types/session_update_params.py index 66dcd351..ed514d97 100644 --- a/src/browserbase/types/session_update_params.py +++ b/src/browserbase/types/session_update_params.py @@ -10,14 +10,15 @@ class SessionUpdateParams(TypedDict, total=False): - project_id: Required[Annotated[str, PropertyInfo(alias="projectId")]] - """The Project ID. - - Can be found in [Settings](https://www.browserbase.com/settings). - """ - status: Required[Literal["REQUEST_RELEASE"]] """Set to `REQUEST_RELEASE` to request that the session complete. Use before session's timeout to avoid additional charges. """ + + project_id: Annotated[str, PropertyInfo(alias="projectId")] + """The Project ID. + + Can be found in [Settings](https://www.browserbase.com/settings). Optional - if + not provided, the project will be inferred from the API key. + """ diff --git a/src/browserbase/types/session_update_response.py b/src/browserbase/types/session_update_response.py index 67a13711..66e677d2 100644 --- a/src/browserbase/types/session_update_response.py +++ b/src/browserbase/types/session_update_response.py @@ -36,17 +36,11 @@ class SessionUpdateResponse(BaseModel): updated_at: datetime = FieldInfo(alias="updatedAt") - avg_cpu_usage: Optional[int] = FieldInfo(alias="avgCpuUsage", default=None) - """CPU used by the Session""" - context_id: Optional[str] = FieldInfo(alias="contextId", default=None) """Optional. The Context linked to the Session.""" ended_at: Optional[datetime] = FieldInfo(alias="endedAt", default=None) - memory_usage: Optional[int] = FieldInfo(alias="memoryUsage", default=None) - """Memory used by the Session""" - user_metadata: Optional[Dict[str, object]] = FieldInfo(alias="userMetadata", default=None) """Arbitrary user metadata to attach to the session. diff --git a/tests/api_resources/test_contexts.py b/tests/api_resources/test_contexts.py index d32ebc4b..1d5f4b44 100644 --- a/tests/api_resources/test_contexts.py +++ b/tests/api_resources/test_contexts.py @@ -23,6 +23,11 @@ class TestContexts: @parametrize def test_method_create(self, client: Browserbase) -> None: + context = client.contexts.create() + assert_matches_type(ContextCreateResponse, context, path=["response"]) + + @parametrize + def test_method_create_with_all_params(self, client: Browserbase) -> None: context = client.contexts.create( project_id="projectId", ) @@ -30,9 +35,7 @@ def test_method_create(self, client: Browserbase) -> None: @parametrize def test_raw_response_create(self, client: Browserbase) -> None: - response = client.contexts.with_raw_response.create( - project_id="projectId", - ) + response = client.contexts.with_raw_response.create() assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -41,9 +44,7 @@ def test_raw_response_create(self, client: Browserbase) -> None: @parametrize def test_streaming_response_create(self, client: Browserbase) -> None: - with client.contexts.with_streaming_response.create( - project_id="projectId", - ) as response: + with client.contexts.with_streaming_response.create() as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -174,6 +175,11 @@ class TestAsyncContexts: @parametrize async def test_method_create(self, async_client: AsyncBrowserbase) -> None: + context = await async_client.contexts.create() + assert_matches_type(ContextCreateResponse, context, path=["response"]) + + @parametrize + async def test_method_create_with_all_params(self, async_client: AsyncBrowserbase) -> None: context = await async_client.contexts.create( project_id="projectId", ) @@ -181,9 +187,7 @@ async def test_method_create(self, async_client: AsyncBrowserbase) -> None: @parametrize async def test_raw_response_create(self, async_client: AsyncBrowserbase) -> None: - response = await async_client.contexts.with_raw_response.create( - project_id="projectId", - ) + response = await async_client.contexts.with_raw_response.create() assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -192,9 +196,7 @@ async def test_raw_response_create(self, async_client: AsyncBrowserbase) -> None @parametrize async def test_streaming_response_create(self, async_client: AsyncBrowserbase) -> None: - async with async_client.contexts.with_streaming_response.create( - project_id="projectId", - ) as response: + async with async_client.contexts.with_streaming_response.create() as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" diff --git a/tests/api_resources/test_sessions.py b/tests/api_resources/test_sessions.py index 7a16f64f..5a597678 100644 --- a/tests/api_resources/test_sessions.py +++ b/tests/api_resources/test_sessions.py @@ -25,15 +25,12 @@ class TestSessions: @parametrize def test_method_create(self, client: Browserbase) -> None: - session = client.sessions.create( - project_id="projectId", - ) + session = client.sessions.create() assert_matches_type(SessionCreateResponse, session, path=["response"]) @parametrize def test_method_create_with_all_params(self, client: Browserbase) -> None: session = client.sessions.create( - project_id="projectId", browser_settings={ "advanced_stealth": True, "block_ads": True, @@ -44,19 +41,6 @@ def test_method_create_with_all_params(self, client: Browserbase) -> None: "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, "os": "windows", "record_session": True, @@ -68,6 +52,7 @@ def test_method_create_with_all_params(self, client: Browserbase) -> None: }, extension_id="extensionId", keep_alive=True, + project_id="projectId", proxies=[ { "type": "browserbase", @@ -87,9 +72,7 @@ def test_method_create_with_all_params(self, client: Browserbase) -> None: @parametrize def test_raw_response_create(self, client: Browserbase) -> None: - response = client.sessions.with_raw_response.create( - project_id="projectId", - ) + response = client.sessions.with_raw_response.create() assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -98,9 +81,7 @@ def test_raw_response_create(self, client: Browserbase) -> None: @parametrize def test_streaming_response_create(self, client: Browserbase) -> None: - with client.sessions.with_streaming_response.create( - project_id="projectId", - ) as response: + with client.sessions.with_streaming_response.create() as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -151,16 +132,23 @@ def test_path_params_retrieve(self, client: Browserbase) -> None: def test_method_update(self, client: Browserbase) -> None: session = client.sessions.update( id="id", - project_id="projectId", status="REQUEST_RELEASE", ) assert_matches_type(SessionUpdateResponse, session, path=["response"]) + @parametrize + def test_method_update_with_all_params(self, client: Browserbase) -> None: + session = client.sessions.update( + id="id", + status="REQUEST_RELEASE", + project_id="projectId", + ) + assert_matches_type(SessionUpdateResponse, session, path=["response"]) + @parametrize def test_raw_response_update(self, client: Browserbase) -> None: response = client.sessions.with_raw_response.update( id="id", - project_id="projectId", status="REQUEST_RELEASE", ) @@ -173,7 +161,6 @@ def test_raw_response_update(self, client: Browserbase) -> None: def test_streaming_response_update(self, client: Browserbase) -> None: with client.sessions.with_streaming_response.update( id="id", - project_id="projectId", status="REQUEST_RELEASE", ) as response: assert not response.is_closed @@ -189,7 +176,6 @@ def test_path_params_update(self, client: Browserbase) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): client.sessions.with_raw_response.update( id="", - project_id="projectId", status="REQUEST_RELEASE", ) @@ -272,15 +258,12 @@ class TestAsyncSessions: @parametrize async def test_method_create(self, async_client: AsyncBrowserbase) -> None: - session = await async_client.sessions.create( - project_id="projectId", - ) + session = await async_client.sessions.create() assert_matches_type(SessionCreateResponse, session, path=["response"]) @parametrize async def test_method_create_with_all_params(self, async_client: AsyncBrowserbase) -> None: session = await async_client.sessions.create( - project_id="projectId", browser_settings={ "advanced_stealth": True, "block_ads": True, @@ -291,19 +274,6 @@ async def test_method_create_with_all_params(self, async_client: AsyncBrowserbas "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, "os": "windows", "record_session": True, @@ -315,6 +285,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncBrowserbas }, extension_id="extensionId", keep_alive=True, + project_id="projectId", proxies=[ { "type": "browserbase", @@ -334,9 +305,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncBrowserbas @parametrize async def test_raw_response_create(self, async_client: AsyncBrowserbase) -> None: - response = await async_client.sessions.with_raw_response.create( - project_id="projectId", - ) + response = await async_client.sessions.with_raw_response.create() assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -345,9 +314,7 @@ async def test_raw_response_create(self, async_client: AsyncBrowserbase) -> None @parametrize async def test_streaming_response_create(self, async_client: AsyncBrowserbase) -> None: - async with async_client.sessions.with_streaming_response.create( - project_id="projectId", - ) as response: + async with async_client.sessions.with_streaming_response.create() as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -398,16 +365,23 @@ async def test_path_params_retrieve(self, async_client: AsyncBrowserbase) -> Non async def test_method_update(self, async_client: AsyncBrowserbase) -> None: session = await async_client.sessions.update( id="id", - project_id="projectId", status="REQUEST_RELEASE", ) assert_matches_type(SessionUpdateResponse, session, path=["response"]) + @parametrize + async def test_method_update_with_all_params(self, async_client: AsyncBrowserbase) -> None: + session = await async_client.sessions.update( + id="id", + status="REQUEST_RELEASE", + project_id="projectId", + ) + assert_matches_type(SessionUpdateResponse, session, path=["response"]) + @parametrize async def test_raw_response_update(self, async_client: AsyncBrowserbase) -> None: response = await async_client.sessions.with_raw_response.update( id="id", - project_id="projectId", status="REQUEST_RELEASE", ) @@ -420,7 +394,6 @@ async def test_raw_response_update(self, async_client: AsyncBrowserbase) -> None async def test_streaming_response_update(self, async_client: AsyncBrowserbase) -> None: async with async_client.sessions.with_streaming_response.update( id="id", - project_id="projectId", status="REQUEST_RELEASE", ) as response: assert not response.is_closed @@ -436,7 +409,6 @@ async def test_path_params_update(self, async_client: AsyncBrowserbase) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): await async_client.sessions.with_raw_response.update( id="", - project_id="projectId", status="REQUEST_RELEASE", ) diff --git a/tests/test_client.py b/tests/test_client.py index 1f33c763..1d0d68b3 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -864,7 +864,7 @@ def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter, clien respx_mock.post("/v1/sessions").mock(side_effect=httpx.TimeoutException("Test timeout error")) with pytest.raises(APITimeoutError): - client.sessions.with_streaming_response.create(project_id="projectId").__enter__() + client.sessions.with_streaming_response.create().__enter__() assert _get_open_connections(client) == 0 @@ -874,7 +874,7 @@ def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter, client respx_mock.post("/v1/sessions").mock(return_value=httpx.Response(500)) with pytest.raises(APIStatusError): - client.sessions.with_streaming_response.create(project_id="projectId").__enter__() + client.sessions.with_streaming_response.create().__enter__() assert _get_open_connections(client) == 0 @pytest.mark.parametrize("failures_before_success", [0, 2, 4]) @@ -903,7 +903,7 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: respx_mock.post("/v1/sessions").mock(side_effect=retry_handler) - response = client.sessions.with_raw_response.create(project_id="projectId") + response = client.sessions.with_raw_response.create() assert response.retries_taken == failures_before_success assert int(response.http_request.headers.get("x-stainless-retry-count")) == failures_before_success @@ -927,9 +927,7 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: respx_mock.post("/v1/sessions").mock(side_effect=retry_handler) - response = client.sessions.with_raw_response.create( - project_id="projectId", extra_headers={"x-stainless-retry-count": Omit()} - ) + response = client.sessions.with_raw_response.create(extra_headers={"x-stainless-retry-count": Omit()}) assert len(response.http_request.headers.get_list("x-stainless-retry-count")) == 0 @@ -952,9 +950,7 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: respx_mock.post("/v1/sessions").mock(side_effect=retry_handler) - response = client.sessions.with_raw_response.create( - project_id="projectId", extra_headers={"x-stainless-retry-count": "42"} - ) + response = client.sessions.with_raw_response.create(extra_headers={"x-stainless-retry-count": "42"}) assert response.http_request.headers.get("x-stainless-retry-count") == "42" @@ -1778,7 +1774,7 @@ async def test_retrying_timeout_errors_doesnt_leak( respx_mock.post("/v1/sessions").mock(side_effect=httpx.TimeoutException("Test timeout error")) with pytest.raises(APITimeoutError): - await async_client.sessions.with_streaming_response.create(project_id="projectId").__aenter__() + await async_client.sessions.with_streaming_response.create().__aenter__() assert _get_open_connections(async_client) == 0 @@ -1790,7 +1786,7 @@ async def test_retrying_status_errors_doesnt_leak( respx_mock.post("/v1/sessions").mock(return_value=httpx.Response(500)) with pytest.raises(APIStatusError): - await async_client.sessions.with_streaming_response.create(project_id="projectId").__aenter__() + await async_client.sessions.with_streaming_response.create().__aenter__() assert _get_open_connections(async_client) == 0 @pytest.mark.parametrize("failures_before_success", [0, 2, 4]) @@ -1819,7 +1815,7 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: respx_mock.post("/v1/sessions").mock(side_effect=retry_handler) - response = await client.sessions.with_raw_response.create(project_id="projectId") + response = await client.sessions.with_raw_response.create() assert response.retries_taken == failures_before_success assert int(response.http_request.headers.get("x-stainless-retry-count")) == failures_before_success @@ -1843,9 +1839,7 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: respx_mock.post("/v1/sessions").mock(side_effect=retry_handler) - response = await client.sessions.with_raw_response.create( - project_id="projectId", extra_headers={"x-stainless-retry-count": Omit()} - ) + response = await client.sessions.with_raw_response.create(extra_headers={"x-stainless-retry-count": Omit()}) assert len(response.http_request.headers.get_list("x-stainless-retry-count")) == 0 @@ -1868,9 +1862,7 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: respx_mock.post("/v1/sessions").mock(side_effect=retry_handler) - response = await client.sessions.with_raw_response.create( - project_id="projectId", extra_headers={"x-stainless-retry-count": "42"} - ) + response = await client.sessions.with_raw_response.create(extra_headers={"x-stainless-retry-count": "42"}) assert response.http_request.headers.get("x-stainless-retry-count") == "42" From a5cfaa98d085e4c4de58e8148cde2e4d56d44071 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 26 Feb 2026 06:37:17 +0000 Subject: [PATCH 254/330] codegen metadata --- .stats.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index a0691ecb..d98d14cf 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 19 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fbrowserbase-a2379f6bf614a1efd1bbb22b2191bf1a3daf09fd42267c8c54ce4284392d1ea4.yml +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fbrowserbase-d64f25e52b0ebf364672eff3f4163eb0a761b5f5865b791027bc5ab467fe535a.yml openapi_spec_hash: 918f5ba73e08f044cfb77de95a8b7524 -config_hash: b01d72cbe03bd762a73b05744086b2ec +config_hash: a106b247c7cdf02ac1033077402cfe2d From eed9f562bd05443b8075259a34baba624869b9f1 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 26 Feb 2026 10:05:33 +0000 Subject: [PATCH 255/330] codegen metadata --- .stats.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index d98d14cf..a0691ecb 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 19 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fbrowserbase-d64f25e52b0ebf364672eff3f4163eb0a761b5f5865b791027bc5ab467fe535a.yml +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fbrowserbase-a2379f6bf614a1efd1bbb22b2191bf1a3daf09fd42267c8c54ce4284392d1ea4.yml openapi_spec_hash: 918f5ba73e08f044cfb77de95a8b7524 -config_hash: a106b247c7cdf02ac1033077402cfe2d +config_hash: b01d72cbe03bd762a73b05744086b2ec From 663899956e398a052b4d0cf3a78e5d7e0718b338 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 26 Feb 2026 18:21:34 +0000 Subject: [PATCH 256/330] feat: [CORE-] Restore models and components in SDK --- .stats.yml | 4 +- README.md | 5 +- api.md | 28 +++++----- src/browserbase/resources/contexts.py | 10 ++-- src/browserbase/resources/extensions.py | 19 ++++--- src/browserbase/resources/projects.py | 20 +++---- .../resources/sessions/sessions.py | 20 +++---- src/browserbase/types/__init__.py | 13 +++-- ...ontext_retrieve_response.py => context.py} | 4 +- ...ension_create_response.py => extension.py} | 4 +- .../types/extension_retrieve_response.py | 22 -------- ...roject_retrieve_response.py => project.py} | 4 +- .../types/project_list_response.py | 27 ++-------- ...ect_usage_response.py => project_usage.py} | 4 +- ...{session_update_response.py => session.py} | 4 +- .../types/session_create_params.py | 22 ++++---- .../types/session_create_response.py | 44 +--------------- .../types/session_list_response.py | 52 ++----------------- ...debug_response.py => session_live_urls.py} | 4 +- .../types/session_retrieve_response.py | 44 ++-------------- src/browserbase/types/sessions/__init__.py | 2 + .../types/sessions/log_list_response.py | 48 ++--------------- .../sessions/recording_retrieve_response.py | 26 ++-------- src/browserbase/types/sessions/session_log.py | 46 ++++++++++++++++ .../types/sessions/session_recording.py | 24 +++++++++ tests/api_resources/test_contexts.py | 18 +++---- tests/api_resources/test_extensions.py | 26 +++++----- tests/api_resources/test_projects.py | 26 +++++----- tests/api_resources/test_sessions.py | 32 ++++++------ 29 files changed, 223 insertions(+), 379 deletions(-) rename src/browserbase/types/{context_retrieve_response.py => context.py} (84%) rename src/browserbase/types/{extension_create_response.py => extension.py} (85%) delete mode 100644 src/browserbase/types/extension_retrieve_response.py rename src/browserbase/types/{project_retrieve_response.py => project.py} (87%) rename src/browserbase/types/{project_usage_response.py => project_usage.py} (78%) rename src/browserbase/types/{session_update_response.py => session.py} (95%) rename src/browserbase/types/{session_debug_response.py => session_live_urls.py} (88%) create mode 100644 src/browserbase/types/sessions/session_log.py create mode 100644 src/browserbase/types/sessions/session_recording.py diff --git a/.stats.yml b/.stats.yml index a0691ecb..311b5669 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 19 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fbrowserbase-a2379f6bf614a1efd1bbb22b2191bf1a3daf09fd42267c8c54ce4284392d1ea4.yml -openapi_spec_hash: 918f5ba73e08f044cfb77de95a8b7524 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fbrowserbase-2cab80bfbbe55ea230b095f214f314e08d30fd5f9cf21781a09dc2925934886a.yml +openapi_spec_hash: c4fadc5bb6b84cd3988c8d864b67bf61 config_hash: b01d72cbe03bd762a73b05744086b2ec diff --git a/README.md b/README.md index 5d384354..89c681fa 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,6 @@ client = Browserbase( session = client.sessions.create( project_id="your_project_id", ) -print(session.id) ``` While you can provide an `api_key` keyword argument, @@ -61,7 +60,6 @@ async def main() -> None: session = await client.sessions.create( project_id="your_project_id", ) - print(session.id) asyncio.run(main()) @@ -97,7 +95,6 @@ async def main() -> None: session = await client.sessions.create( project_id="your_project_id", ) - print(session.id) asyncio.run(main()) @@ -279,7 +276,7 @@ response = client.sessions.with_raw_response.create( print(response.headers.get('X-My-Header')) session = response.parse() # get the object that `sessions.create()` would have returned -print(session.id) +print(session) ``` These methods return an [`APIResponse`](https://github.com/browserbase/sdk-python/tree/main/src/browserbase/_response.py) object. diff --git a/api.md b/api.md index 7e8a3f13..d2d26e57 100644 --- a/api.md +++ b/api.md @@ -3,13 +3,13 @@ Types: ```python -from browserbase.types import ContextCreateResponse, ContextRetrieveResponse, ContextUpdateResponse +from browserbase.types import Context, ContextCreateResponse, ContextUpdateResponse ``` Methods: - client.contexts.create(\*\*params) -> ContextCreateResponse -- client.contexts.retrieve(id) -> ContextRetrieveResponse +- client.contexts.retrieve(id) -> Context - client.contexts.update(id) -> ContextUpdateResponse - client.contexts.delete(id) -> None @@ -18,13 +18,13 @@ Methods: Types: ```python -from browserbase.types import ExtensionCreateResponse, ExtensionRetrieveResponse +from browserbase.types import Extension ``` Methods: -- client.extensions.create(\*\*params) -> ExtensionCreateResponse -- client.extensions.retrieve(id) -> ExtensionRetrieveResponse +- client.extensions.create(\*\*params) -> Extension +- client.extensions.retrieve(id) -> Extension - client.extensions.delete(id) -> None # Projects @@ -32,14 +32,14 @@ Methods: Types: ```python -from browserbase.types import ProjectRetrieveResponse, ProjectListResponse, ProjectUsageResponse +from browserbase.types import Project, ProjectUsage, ProjectListResponse ``` Methods: -- client.projects.retrieve(id) -> ProjectRetrieveResponse +- client.projects.retrieve(id) -> Project - client.projects.list() -> ProjectListResponse -- client.projects.usage(id) -> ProjectUsageResponse +- client.projects.usage(id) -> ProjectUsage # Sessions @@ -47,11 +47,11 @@ Types: ```python from browserbase.types import ( + Session, + SessionLiveURLs, SessionCreateResponse, SessionRetrieveResponse, - SessionUpdateResponse, SessionListResponse, - SessionDebugResponse, ) ``` @@ -59,9 +59,9 @@ Methods: - client.sessions.create(\*\*params) -> SessionCreateResponse - client.sessions.retrieve(id) -> SessionRetrieveResponse -- client.sessions.update(id, \*\*params) -> SessionUpdateResponse +- client.sessions.update(id, \*\*params) -> Session - client.sessions.list(\*\*params) -> SessionListResponse -- client.sessions.debug(id) -> SessionDebugResponse +- client.sessions.debug(id) -> SessionLiveURLs ## Downloads @@ -74,7 +74,7 @@ Methods: Types: ```python -from browserbase.types.sessions import LogListResponse +from browserbase.types.sessions import SessionLog, LogListResponse ``` Methods: @@ -86,7 +86,7 @@ Methods: Types: ```python -from browserbase.types.sessions import RecordingRetrieveResponse +from browserbase.types.sessions import SessionRecording, RecordingRetrieveResponse ``` Methods: diff --git a/src/browserbase/resources/contexts.py b/src/browserbase/resources/contexts.py index 989fd48b..685daee4 100644 --- a/src/browserbase/resources/contexts.py +++ b/src/browserbase/resources/contexts.py @@ -16,9 +16,9 @@ async_to_streamed_response_wrapper, ) from .._base_client import make_request_options +from ..types.context import Context from ..types.context_create_response import ContextCreateResponse from ..types.context_update_response import ContextUpdateResponse -from ..types.context_retrieve_response import ContextRetrieveResponse __all__ = ["ContextsResource", "AsyncContextsResource"] @@ -90,7 +90,7 @@ def retrieve( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> ContextRetrieveResponse: + ) -> Context: """ Get a Context @@ -110,7 +110,7 @@ def retrieve( options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=ContextRetrieveResponse, + cast_to=Context, ) def update( @@ -248,7 +248,7 @@ async def retrieve( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> ContextRetrieveResponse: + ) -> Context: """ Get a Context @@ -268,7 +268,7 @@ async def retrieve( options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=ContextRetrieveResponse, + cast_to=Context, ) async def update( diff --git a/src/browserbase/resources/extensions.py b/src/browserbase/resources/extensions.py index 21d06e70..534cd415 100644 --- a/src/browserbase/resources/extensions.py +++ b/src/browserbase/resources/extensions.py @@ -18,8 +18,7 @@ async_to_streamed_response_wrapper, ) from .._base_client import make_request_options -from ..types.extension_create_response import ExtensionCreateResponse -from ..types.extension_retrieve_response import ExtensionRetrieveResponse +from ..types.extension import Extension __all__ = ["ExtensionsResource", "AsyncExtensionsResource"] @@ -54,7 +53,7 @@ def create( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> ExtensionCreateResponse: + ) -> Extension: """ Upload an Extension @@ -80,7 +79,7 @@ def create( options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=ExtensionCreateResponse, + cast_to=Extension, ) def retrieve( @@ -93,7 +92,7 @@ def retrieve( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> ExtensionRetrieveResponse: + ) -> Extension: """ Get an Extension @@ -113,7 +112,7 @@ def retrieve( options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=ExtensionRetrieveResponse, + cast_to=Extension, ) def delete( @@ -181,7 +180,7 @@ async def create( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> ExtensionCreateResponse: + ) -> Extension: """ Upload an Extension @@ -207,7 +206,7 @@ async def create( options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=ExtensionCreateResponse, + cast_to=Extension, ) async def retrieve( @@ -220,7 +219,7 @@ async def retrieve( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> ExtensionRetrieveResponse: + ) -> Extension: """ Get an Extension @@ -240,7 +239,7 @@ async def retrieve( options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=ExtensionRetrieveResponse, + cast_to=Extension, ) async def delete( diff --git a/src/browserbase/resources/projects.py b/src/browserbase/resources/projects.py index 62c28afa..5ce225fd 100644 --- a/src/browserbase/resources/projects.py +++ b/src/browserbase/resources/projects.py @@ -14,9 +14,9 @@ async_to_streamed_response_wrapper, ) from .._base_client import make_request_options +from ..types.project import Project +from ..types.project_usage import ProjectUsage from ..types.project_list_response import ProjectListResponse -from ..types.project_usage_response import ProjectUsageResponse -from ..types.project_retrieve_response import ProjectRetrieveResponse __all__ = ["ProjectsResource", "AsyncProjectsResource"] @@ -51,7 +51,7 @@ def retrieve( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> ProjectRetrieveResponse: + ) -> Project: """ Get a Project @@ -71,7 +71,7 @@ def retrieve( options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=ProjectRetrieveResponse, + cast_to=Project, ) def list( @@ -103,7 +103,7 @@ def usage( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> ProjectUsageResponse: + ) -> ProjectUsage: """ Get Project Usage @@ -123,7 +123,7 @@ def usage( options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=ProjectUsageResponse, + cast_to=ProjectUsage, ) @@ -157,7 +157,7 @@ async def retrieve( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> ProjectRetrieveResponse: + ) -> Project: """ Get a Project @@ -177,7 +177,7 @@ async def retrieve( options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=ProjectRetrieveResponse, + cast_to=Project, ) async def list( @@ -209,7 +209,7 @@ async def usage( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> ProjectUsageResponse: + ) -> ProjectUsage: """ Get Project Usage @@ -229,7 +229,7 @@ async def usage( options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=ProjectUsageResponse, + cast_to=ProjectUsage, ) diff --git a/src/browserbase/resources/sessions/sessions.py b/src/browserbase/resources/sessions/sessions.py index e7a28510..35ff90ce 100644 --- a/src/browserbase/resources/sessions/sessions.py +++ b/src/browserbase/resources/sessions/sessions.py @@ -51,10 +51,10 @@ async_to_streamed_response_wrapper, ) from ..._base_client import make_request_options +from ...types.session import Session +from ...types.session_live_urls import SessionLiveURLs from ...types.session_list_response import SessionListResponse -from ...types.session_debug_response import SessionDebugResponse from ...types.session_create_response import SessionCreateResponse -from ...types.session_update_response import SessionUpdateResponse from ...types.session_retrieve_response import SessionRetrieveResponse __all__ = ["SessionsResource", "AsyncSessionsResource"] @@ -214,7 +214,7 @@ def update( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> SessionUpdateResponse: + ) -> Session: """ Update a Session @@ -248,7 +248,7 @@ def update( options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=SessionUpdateResponse, + cast_to=Session, ) def list( @@ -308,7 +308,7 @@ def debug( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> SessionDebugResponse: + ) -> SessionLiveURLs: """ Session Live URLs @@ -328,7 +328,7 @@ def debug( options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=SessionDebugResponse, + cast_to=SessionLiveURLs, ) @@ -486,7 +486,7 @@ async def update( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> SessionUpdateResponse: + ) -> Session: """ Update a Session @@ -520,7 +520,7 @@ async def update( options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=SessionUpdateResponse, + cast_to=Session, ) async def list( @@ -580,7 +580,7 @@ async def debug( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> SessionDebugResponse: + ) -> SessionLiveURLs: """ Session Live URLs @@ -600,7 +600,7 @@ async def debug( options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=SessionDebugResponse, + cast_to=SessionLiveURLs, ) diff --git a/src/browserbase/types/__init__.py b/src/browserbase/types/__init__.py index 20e2f905..4dd85ddb 100644 --- a/src/browserbase/types/__init__.py +++ b/src/browserbase/types/__init__.py @@ -2,21 +2,20 @@ from __future__ import annotations +from .context import Context as Context +from .project import Project as Project +from .session import Session as Session +from .extension import Extension as Extension +from .project_usage import ProjectUsage as ProjectUsage +from .session_live_urls import SessionLiveURLs as SessionLiveURLs from .session_list_params import SessionListParams as SessionListParams from .context_create_params import ContextCreateParams as ContextCreateParams from .project_list_response import ProjectListResponse as ProjectListResponse from .session_create_params import SessionCreateParams as SessionCreateParams from .session_list_response import SessionListResponse as SessionListResponse from .session_update_params import SessionUpdateParams as SessionUpdateParams -from .project_usage_response import ProjectUsageResponse as ProjectUsageResponse -from .session_debug_response import SessionDebugResponse as SessionDebugResponse from .context_create_response import ContextCreateResponse as ContextCreateResponse from .context_update_response import ContextUpdateResponse as ContextUpdateResponse from .extension_create_params import ExtensionCreateParams as ExtensionCreateParams from .session_create_response import SessionCreateResponse as SessionCreateResponse -from .session_update_response import SessionUpdateResponse as SessionUpdateResponse -from .context_retrieve_response import ContextRetrieveResponse as ContextRetrieveResponse -from .extension_create_response import ExtensionCreateResponse as ExtensionCreateResponse -from .project_retrieve_response import ProjectRetrieveResponse as ProjectRetrieveResponse from .session_retrieve_response import SessionRetrieveResponse as SessionRetrieveResponse -from .extension_retrieve_response import ExtensionRetrieveResponse as ExtensionRetrieveResponse diff --git a/src/browserbase/types/context_retrieve_response.py b/src/browserbase/types/context.py similarity index 84% rename from src/browserbase/types/context_retrieve_response.py rename to src/browserbase/types/context.py index c2cd6925..cb5c32fd 100644 --- a/src/browserbase/types/context_retrieve_response.py +++ b/src/browserbase/types/context.py @@ -6,10 +6,10 @@ from .._models import BaseModel -__all__ = ["ContextRetrieveResponse"] +__all__ = ["Context"] -class ContextRetrieveResponse(BaseModel): +class Context(BaseModel): id: str created_at: datetime = FieldInfo(alias="createdAt") diff --git a/src/browserbase/types/extension_create_response.py b/src/browserbase/types/extension.py similarity index 85% rename from src/browserbase/types/extension_create_response.py rename to src/browserbase/types/extension.py index d2b74f41..94582c34 100644 --- a/src/browserbase/types/extension_create_response.py +++ b/src/browserbase/types/extension.py @@ -6,10 +6,10 @@ from .._models import BaseModel -__all__ = ["ExtensionCreateResponse"] +__all__ = ["Extension"] -class ExtensionCreateResponse(BaseModel): +class Extension(BaseModel): id: str created_at: datetime = FieldInfo(alias="createdAt") diff --git a/src/browserbase/types/extension_retrieve_response.py b/src/browserbase/types/extension_retrieve_response.py deleted file mode 100644 index c786348e..00000000 --- a/src/browserbase/types/extension_retrieve_response.py +++ /dev/null @@ -1,22 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from datetime import datetime - -from pydantic import Field as FieldInfo - -from .._models import BaseModel - -__all__ = ["ExtensionRetrieveResponse"] - - -class ExtensionRetrieveResponse(BaseModel): - id: str - - created_at: datetime = FieldInfo(alias="createdAt") - - file_name: str = FieldInfo(alias="fileName") - - project_id: str = FieldInfo(alias="projectId") - """The Project ID linked to the uploaded Extension.""" - - updated_at: datetime = FieldInfo(alias="updatedAt") diff --git a/src/browserbase/types/project_retrieve_response.py b/src/browserbase/types/project.py similarity index 87% rename from src/browserbase/types/project_retrieve_response.py rename to src/browserbase/types/project.py index 78126679..dc3cf335 100644 --- a/src/browserbase/types/project_retrieve_response.py +++ b/src/browserbase/types/project.py @@ -6,10 +6,10 @@ from .._models import BaseModel -__all__ = ["ProjectRetrieveResponse"] +__all__ = ["Project"] -class ProjectRetrieveResponse(BaseModel): +class Project(BaseModel): id: str concurrency: int diff --git a/src/browserbase/types/project_list_response.py b/src/browserbase/types/project_list_response.py index e364b520..2d05a236 100644 --- a/src/browserbase/types/project_list_response.py +++ b/src/browserbase/types/project_list_response.py @@ -1,31 +1,10 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. from typing import List -from datetime import datetime from typing_extensions import TypeAlias -from pydantic import Field as FieldInfo +from .project import Project -from .._models import BaseModel +__all__ = ["ProjectListResponse"] -__all__ = ["ProjectListResponse", "ProjectListResponseItem"] - - -class ProjectListResponseItem(BaseModel): - id: str - - concurrency: int - """The maximum number of sessions that this project can run concurrently.""" - - created_at: datetime = FieldInfo(alias="createdAt") - - default_timeout: int = FieldInfo(alias="defaultTimeout") - - name: str - - owner_id: str = FieldInfo(alias="ownerId") - - updated_at: datetime = FieldInfo(alias="updatedAt") - - -ProjectListResponse: TypeAlias = List[ProjectListResponseItem] +ProjectListResponse: TypeAlias = List[Project] diff --git a/src/browserbase/types/project_usage_response.py b/src/browserbase/types/project_usage.py similarity index 78% rename from src/browserbase/types/project_usage_response.py rename to src/browserbase/types/project_usage.py index b52fccfe..c8a03f5b 100644 --- a/src/browserbase/types/project_usage_response.py +++ b/src/browserbase/types/project_usage.py @@ -4,10 +4,10 @@ from .._models import BaseModel -__all__ = ["ProjectUsageResponse"] +__all__ = ["ProjectUsage"] -class ProjectUsageResponse(BaseModel): +class ProjectUsage(BaseModel): browser_minutes: int = FieldInfo(alias="browserMinutes") proxy_bytes: int = FieldInfo(alias="proxyBytes") diff --git a/src/browserbase/types/session_update_response.py b/src/browserbase/types/session.py similarity index 95% rename from src/browserbase/types/session_update_response.py rename to src/browserbase/types/session.py index 66e677d2..e983baaa 100644 --- a/src/browserbase/types/session_update_response.py +++ b/src/browserbase/types/session.py @@ -8,10 +8,10 @@ from .._models import BaseModel -__all__ = ["SessionUpdateResponse"] +__all__ = ["Session"] -class SessionUpdateResponse(BaseModel): +class Session(BaseModel): id: str created_at: datetime = FieldInfo(alias="createdAt") diff --git a/src/browserbase/types/session_create_params.py b/src/browserbase/types/session_create_params.py index 17629fe2..63805590 100644 --- a/src/browserbase/types/session_create_params.py +++ b/src/browserbase/types/session_create_params.py @@ -13,10 +13,10 @@ "BrowserSettingsContext", "BrowserSettingsViewport", "ProxiesUnionMember0", - "ProxiesUnionMember0UnionMember0", - "ProxiesUnionMember0UnionMember0Geolocation", - "ProxiesUnionMember0UnionMember1", - "ProxiesUnionMember0UnionMember2", + "ProxiesUnionMember0BrowserbaseProxyConfig", + "ProxiesUnionMember0BrowserbaseProxyConfigGeolocation", + "ProxiesUnionMember0ExternalProxyConfig", + "ProxiesUnionMember0NoneProxyConfig", ] @@ -126,7 +126,7 @@ class BrowserSettings(TypedDict, total=False): viewport: BrowserSettingsViewport -class ProxiesUnionMember0UnionMember0Geolocation(TypedDict, total=False): +class ProxiesUnionMember0BrowserbaseProxyConfigGeolocation(TypedDict, total=False): """Geographic location for the proxy. Optional.""" country: Required[str] @@ -139,7 +139,7 @@ class ProxiesUnionMember0UnionMember0Geolocation(TypedDict, total=False): """US state code (2 characters). Must also specify US as the country. Optional.""" -class ProxiesUnionMember0UnionMember0(TypedDict, total=False): +class ProxiesUnionMember0BrowserbaseProxyConfig(TypedDict, total=False): type: Required[Literal["browserbase"]] """Type of proxy. @@ -152,11 +152,11 @@ class ProxiesUnionMember0UnionMember0(TypedDict, total=False): If omitted, defaults to all domains. Optional. """ - geolocation: ProxiesUnionMember0UnionMember0Geolocation + geolocation: ProxiesUnionMember0BrowserbaseProxyConfigGeolocation """Geographic location for the proxy. Optional.""" -class ProxiesUnionMember0UnionMember1(TypedDict, total=False): +class ProxiesUnionMember0ExternalProxyConfig(TypedDict, total=False): server: Required[str] """Server URL for external proxy. Required.""" @@ -176,7 +176,7 @@ class ProxiesUnionMember0UnionMember1(TypedDict, total=False): """Username for external proxy authentication. Optional.""" -class ProxiesUnionMember0UnionMember2(TypedDict, total=False): +class ProxiesUnionMember0NoneProxyConfig(TypedDict, total=False): type: Required[Literal["none"]] """Type of proxy. Always 'none' for this config.""" @@ -188,5 +188,7 @@ class ProxiesUnionMember0UnionMember2(TypedDict, total=False): ProxiesUnionMember0: TypeAlias = Union[ - ProxiesUnionMember0UnionMember0, ProxiesUnionMember0UnionMember1, ProxiesUnionMember0UnionMember2 + ProxiesUnionMember0BrowserbaseProxyConfig, + ProxiesUnionMember0ExternalProxyConfig, + ProxiesUnionMember0NoneProxyConfig, ] diff --git a/src/browserbase/types/session_create_response.py b/src/browserbase/types/session_create_response.py index 7a863d14..c91a4d09 100644 --- a/src/browserbase/types/session_create_response.py +++ b/src/browserbase/types/session_create_response.py @@ -1,58 +1,18 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -from typing import Dict, Optional -from datetime import datetime -from typing_extensions import Literal - from pydantic import Field as FieldInfo -from .._models import BaseModel +from .session import Session __all__ = ["SessionCreateResponse"] -class SessionCreateResponse(BaseModel): - id: str - +class SessionCreateResponse(Session): connect_url: str = FieldInfo(alias="connectUrl") """WebSocket URL to connect to the Session.""" - created_at: datetime = FieldInfo(alias="createdAt") - - expires_at: datetime = FieldInfo(alias="expiresAt") - - keep_alive: bool = FieldInfo(alias="keepAlive") - """Indicates if the Session was created to be kept alive upon disconnections""" - - project_id: str = FieldInfo(alias="projectId") - """The Project ID linked to the Session.""" - - proxy_bytes: int = FieldInfo(alias="proxyBytes") - """Bytes used via the [Proxy](/features/stealth-mode#proxies-and-residential-ips)""" - - region: Literal["us-west-2", "us-east-1", "eu-central-1", "ap-southeast-1"] - """The region where the Session is running.""" - selenium_remote_url: str = FieldInfo(alias="seleniumRemoteUrl") """HTTP URL to connect to the Session.""" signing_key: str = FieldInfo(alias="signingKey") """Signing key to use when connecting to the Session via HTTP.""" - - started_at: datetime = FieldInfo(alias="startedAt") - - status: Literal["RUNNING", "ERROR", "TIMED_OUT", "COMPLETED"] - - updated_at: datetime = FieldInfo(alias="updatedAt") - - context_id: Optional[str] = FieldInfo(alias="contextId", default=None) - """Optional. The Context linked to the Session.""" - - ended_at: Optional[datetime] = FieldInfo(alias="endedAt", default=None) - - user_metadata: Optional[Dict[str, object]] = FieldInfo(alias="userMetadata", default=None) - """Arbitrary user metadata to attach to the session. - - To learn more about user metadata, see - [User Metadata](/features/sessions#user-metadata). - """ diff --git a/src/browserbase/types/session_list_response.py b/src/browserbase/types/session_list_response.py index 103c2147..ca162ddb 100644 --- a/src/browserbase/types/session_list_response.py +++ b/src/browserbase/types/session_list_response.py @@ -1,52 +1,10 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -from typing import Dict, List, Optional -from datetime import datetime -from typing_extensions import Literal, TypeAlias +from typing import List +from typing_extensions import TypeAlias -from pydantic import Field as FieldInfo +from .session import Session -from .._models import BaseModel +__all__ = ["SessionListResponse"] -__all__ = ["SessionListResponse", "SessionListResponseItem"] - - -class SessionListResponseItem(BaseModel): - id: str - - created_at: datetime = FieldInfo(alias="createdAt") - - expires_at: datetime = FieldInfo(alias="expiresAt") - - keep_alive: bool = FieldInfo(alias="keepAlive") - """Indicates if the Session was created to be kept alive upon disconnections""" - - project_id: str = FieldInfo(alias="projectId") - """The Project ID linked to the Session.""" - - proxy_bytes: int = FieldInfo(alias="proxyBytes") - """Bytes used via the [Proxy](/features/stealth-mode#proxies-and-residential-ips)""" - - region: Literal["us-west-2", "us-east-1", "eu-central-1", "ap-southeast-1"] - """The region where the Session is running.""" - - started_at: datetime = FieldInfo(alias="startedAt") - - status: Literal["RUNNING", "ERROR", "TIMED_OUT", "COMPLETED"] - - updated_at: datetime = FieldInfo(alias="updatedAt") - - context_id: Optional[str] = FieldInfo(alias="contextId", default=None) - """Optional. The Context linked to the Session.""" - - ended_at: Optional[datetime] = FieldInfo(alias="endedAt", default=None) - - user_metadata: Optional[Dict[str, object]] = FieldInfo(alias="userMetadata", default=None) - """Arbitrary user metadata to attach to the session. - - To learn more about user metadata, see - [User Metadata](/features/sessions#user-metadata). - """ - - -SessionListResponse: TypeAlias = List[SessionListResponseItem] +SessionListResponse: TypeAlias = List[Session] diff --git a/src/browserbase/types/session_debug_response.py b/src/browserbase/types/session_live_urls.py similarity index 88% rename from src/browserbase/types/session_debug_response.py rename to src/browserbase/types/session_live_urls.py index 9cee7a77..3c7ba320 100644 --- a/src/browserbase/types/session_debug_response.py +++ b/src/browserbase/types/session_live_urls.py @@ -6,7 +6,7 @@ from .._models import BaseModel -__all__ = ["SessionDebugResponse", "Page"] +__all__ = ["SessionLiveURLs", "Page"] class Page(BaseModel): @@ -23,7 +23,7 @@ class Page(BaseModel): url: str -class SessionDebugResponse(BaseModel): +class SessionLiveURLs(BaseModel): debugger_fullscreen_url: str = FieldInfo(alias="debuggerFullscreenUrl") debugger_url: str = FieldInfo(alias="debuggerUrl") diff --git a/src/browserbase/types/session_retrieve_response.py b/src/browserbase/types/session_retrieve_response.py index 885c281e..2203db0d 100644 --- a/src/browserbase/types/session_retrieve_response.py +++ b/src/browserbase/types/session_retrieve_response.py @@ -1,58 +1,20 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -from typing import Dict, Optional -from datetime import datetime -from typing_extensions import Literal +from typing import Optional from pydantic import Field as FieldInfo -from .._models import BaseModel +from .session import Session __all__ = ["SessionRetrieveResponse"] -class SessionRetrieveResponse(BaseModel): - id: str - - created_at: datetime = FieldInfo(alias="createdAt") - - expires_at: datetime = FieldInfo(alias="expiresAt") - - keep_alive: bool = FieldInfo(alias="keepAlive") - """Indicates if the Session was created to be kept alive upon disconnections""" - - project_id: str = FieldInfo(alias="projectId") - """The Project ID linked to the Session.""" - - proxy_bytes: int = FieldInfo(alias="proxyBytes") - """Bytes used via the [Proxy](/features/stealth-mode#proxies-and-residential-ips)""" - - region: Literal["us-west-2", "us-east-1", "eu-central-1", "ap-southeast-1"] - """The region where the Session is running.""" - - started_at: datetime = FieldInfo(alias="startedAt") - - status: Literal["RUNNING", "ERROR", "TIMED_OUT", "COMPLETED"] - - updated_at: datetime = FieldInfo(alias="updatedAt") - +class SessionRetrieveResponse(Session): connect_url: Optional[str] = FieldInfo(alias="connectUrl", default=None) """WebSocket URL to connect to the Session.""" - context_id: Optional[str] = FieldInfo(alias="contextId", default=None) - """Optional. The Context linked to the Session.""" - - ended_at: Optional[datetime] = FieldInfo(alias="endedAt", default=None) - selenium_remote_url: Optional[str] = FieldInfo(alias="seleniumRemoteUrl", default=None) """HTTP URL to connect to the Session.""" signing_key: Optional[str] = FieldInfo(alias="signingKey", default=None) """Signing key to use when connecting to the Session via HTTP.""" - - user_metadata: Optional[Dict[str, object]] = FieldInfo(alias="userMetadata", default=None) - """Arbitrary user metadata to attach to the session. - - To learn more about user metadata, see - [User Metadata](/features/sessions#user-metadata). - """ diff --git a/src/browserbase/types/sessions/__init__.py b/src/browserbase/types/sessions/__init__.py index 69d54703..0cef6b19 100644 --- a/src/browserbase/types/sessions/__init__.py +++ b/src/browserbase/types/sessions/__init__.py @@ -2,7 +2,9 @@ from __future__ import annotations +from .session_log import SessionLog as SessionLog from .log_list_response import LogListResponse as LogListResponse +from .session_recording import SessionRecording as SessionRecording from .upload_create_params import UploadCreateParams as UploadCreateParams from .upload_create_response import UploadCreateResponse as UploadCreateResponse from .recording_retrieve_response import RecordingRetrieveResponse as RecordingRetrieveResponse diff --git a/src/browserbase/types/sessions/log_list_response.py b/src/browserbase/types/sessions/log_list_response.py index efd848ab..2b325a8c 100644 --- a/src/browserbase/types/sessions/log_list_response.py +++ b/src/browserbase/types/sessions/log_list_response.py @@ -1,50 +1,10 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -from typing import Dict, List, Optional +from typing import List from typing_extensions import TypeAlias -from pydantic import Field as FieldInfo +from .session_log import SessionLog -from ..._models import BaseModel +__all__ = ["LogListResponse"] -__all__ = ["LogListResponse", "LogListResponseItem", "LogListResponseItemRequest", "LogListResponseItemResponse"] - - -class LogListResponseItemRequest(BaseModel): - params: Dict[str, object] - - raw_body: str = FieldInfo(alias="rawBody") - - timestamp: Optional[int] = None - """milliseconds that have elapsed since the UNIX epoch""" - - -class LogListResponseItemResponse(BaseModel): - raw_body: str = FieldInfo(alias="rawBody") - - result: Dict[str, object] - - timestamp: Optional[int] = None - """milliseconds that have elapsed since the UNIX epoch""" - - -class LogListResponseItem(BaseModel): - method: str - - page_id: int = FieldInfo(alias="pageId") - - session_id: str = FieldInfo(alias="sessionId") - - frame_id: Optional[str] = FieldInfo(alias="frameId", default=None) - - loader_id: Optional[str] = FieldInfo(alias="loaderId", default=None) - - request: Optional[LogListResponseItemRequest] = None - - response: Optional[LogListResponseItemResponse] = None - - timestamp: Optional[int] = None - """milliseconds that have elapsed since the UNIX epoch""" - - -LogListResponse: TypeAlias = List[LogListResponseItem] +LogListResponse: TypeAlias = List[SessionLog] diff --git a/src/browserbase/types/sessions/recording_retrieve_response.py b/src/browserbase/types/sessions/recording_retrieve_response.py index d3613b8c..951969bb 100644 --- a/src/browserbase/types/sessions/recording_retrieve_response.py +++ b/src/browserbase/types/sessions/recording_retrieve_response.py @@ -1,28 +1,10 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -from typing import Dict, List +from typing import List from typing_extensions import TypeAlias -from pydantic import Field as FieldInfo +from .session_recording import SessionRecording -from ..._models import BaseModel +__all__ = ["RecordingRetrieveResponse"] -__all__ = ["RecordingRetrieveResponse", "RecordingRetrieveResponseItem"] - - -class RecordingRetrieveResponseItem(BaseModel): - data: Dict[str, object] - """ - See - [rrweb documentation](https://github.com/rrweb-io/rrweb/blob/master/docs/recipes/dive-into-event.md). - """ - - session_id: str = FieldInfo(alias="sessionId") - - timestamp: int - """milliseconds that have elapsed since the UNIX epoch""" - - type: int - - -RecordingRetrieveResponse: TypeAlias = List[RecordingRetrieveResponseItem] +RecordingRetrieveResponse: TypeAlias = List[SessionRecording] diff --git a/src/browserbase/types/sessions/session_log.py b/src/browserbase/types/sessions/session_log.py new file mode 100644 index 00000000..428f518a --- /dev/null +++ b/src/browserbase/types/sessions/session_log.py @@ -0,0 +1,46 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Dict, Optional + +from pydantic import Field as FieldInfo + +from ..._models import BaseModel + +__all__ = ["SessionLog", "Request", "Response"] + + +class Request(BaseModel): + params: Dict[str, object] + + raw_body: str = FieldInfo(alias="rawBody") + + timestamp: Optional[int] = None + """milliseconds that have elapsed since the UNIX epoch""" + + +class Response(BaseModel): + raw_body: str = FieldInfo(alias="rawBody") + + result: Dict[str, object] + + timestamp: Optional[int] = None + """milliseconds that have elapsed since the UNIX epoch""" + + +class SessionLog(BaseModel): + method: str + + page_id: int = FieldInfo(alias="pageId") + + session_id: str = FieldInfo(alias="sessionId") + + frame_id: Optional[str] = FieldInfo(alias="frameId", default=None) + + loader_id: Optional[str] = FieldInfo(alias="loaderId", default=None) + + request: Optional[Request] = None + + response: Optional[Response] = None + + timestamp: Optional[int] = None + """milliseconds that have elapsed since the UNIX epoch""" diff --git a/src/browserbase/types/sessions/session_recording.py b/src/browserbase/types/sessions/session_recording.py new file mode 100644 index 00000000..c8471371 --- /dev/null +++ b/src/browserbase/types/sessions/session_recording.py @@ -0,0 +1,24 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Dict + +from pydantic import Field as FieldInfo + +from ..._models import BaseModel + +__all__ = ["SessionRecording"] + + +class SessionRecording(BaseModel): + data: Dict[str, object] + """ + See + [rrweb documentation](https://github.com/rrweb-io/rrweb/blob/master/docs/recipes/dive-into-event.md). + """ + + session_id: str = FieldInfo(alias="sessionId") + + timestamp: int + """milliseconds that have elapsed since the UNIX epoch""" + + type: int diff --git a/tests/api_resources/test_contexts.py b/tests/api_resources/test_contexts.py index 1d5f4b44..31fb97d0 100644 --- a/tests/api_resources/test_contexts.py +++ b/tests/api_resources/test_contexts.py @@ -9,11 +9,7 @@ from browserbase import Browserbase, AsyncBrowserbase from tests.utils import assert_matches_type -from browserbase.types import ( - ContextCreateResponse, - ContextUpdateResponse, - ContextRetrieveResponse, -) +from browserbase.types import Context, ContextCreateResponse, ContextUpdateResponse base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") @@ -58,7 +54,7 @@ def test_method_retrieve(self, client: Browserbase) -> None: context = client.contexts.retrieve( "id", ) - assert_matches_type(ContextRetrieveResponse, context, path=["response"]) + assert_matches_type(Context, context, path=["response"]) @parametrize def test_raw_response_retrieve(self, client: Browserbase) -> None: @@ -69,7 +65,7 @@ def test_raw_response_retrieve(self, client: Browserbase) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" context = response.parse() - assert_matches_type(ContextRetrieveResponse, context, path=["response"]) + assert_matches_type(Context, context, path=["response"]) @parametrize def test_streaming_response_retrieve(self, client: Browserbase) -> None: @@ -80,7 +76,7 @@ def test_streaming_response_retrieve(self, client: Browserbase) -> None: assert response.http_request.headers.get("X-Stainless-Lang") == "python" context = response.parse() - assert_matches_type(ContextRetrieveResponse, context, path=["response"]) + assert_matches_type(Context, context, path=["response"]) assert cast(Any, response.is_closed) is True @@ -210,7 +206,7 @@ async def test_method_retrieve(self, async_client: AsyncBrowserbase) -> None: context = await async_client.contexts.retrieve( "id", ) - assert_matches_type(ContextRetrieveResponse, context, path=["response"]) + assert_matches_type(Context, context, path=["response"]) @parametrize async def test_raw_response_retrieve(self, async_client: AsyncBrowserbase) -> None: @@ -221,7 +217,7 @@ async def test_raw_response_retrieve(self, async_client: AsyncBrowserbase) -> No assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" context = await response.parse() - assert_matches_type(ContextRetrieveResponse, context, path=["response"]) + assert_matches_type(Context, context, path=["response"]) @parametrize async def test_streaming_response_retrieve(self, async_client: AsyncBrowserbase) -> None: @@ -232,7 +228,7 @@ async def test_streaming_response_retrieve(self, async_client: AsyncBrowserbase) assert response.http_request.headers.get("X-Stainless-Lang") == "python" context = await response.parse() - assert_matches_type(ContextRetrieveResponse, context, path=["response"]) + assert_matches_type(Context, context, path=["response"]) assert cast(Any, response.is_closed) is True diff --git a/tests/api_resources/test_extensions.py b/tests/api_resources/test_extensions.py index e32ae9b0..6b6a0183 100644 --- a/tests/api_resources/test_extensions.py +++ b/tests/api_resources/test_extensions.py @@ -9,7 +9,7 @@ from browserbase import Browserbase, AsyncBrowserbase from tests.utils import assert_matches_type -from browserbase.types import ExtensionCreateResponse, ExtensionRetrieveResponse +from browserbase.types import Extension base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") @@ -22,7 +22,7 @@ def test_method_create(self, client: Browserbase) -> None: extension = client.extensions.create( file=b"raw file contents", ) - assert_matches_type(ExtensionCreateResponse, extension, path=["response"]) + assert_matches_type(Extension, extension, path=["response"]) @parametrize def test_raw_response_create(self, client: Browserbase) -> None: @@ -33,7 +33,7 @@ def test_raw_response_create(self, client: Browserbase) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" extension = response.parse() - assert_matches_type(ExtensionCreateResponse, extension, path=["response"]) + assert_matches_type(Extension, extension, path=["response"]) @parametrize def test_streaming_response_create(self, client: Browserbase) -> None: @@ -44,7 +44,7 @@ def test_streaming_response_create(self, client: Browserbase) -> None: assert response.http_request.headers.get("X-Stainless-Lang") == "python" extension = response.parse() - assert_matches_type(ExtensionCreateResponse, extension, path=["response"]) + assert_matches_type(Extension, extension, path=["response"]) assert cast(Any, response.is_closed) is True @@ -53,7 +53,7 @@ def test_method_retrieve(self, client: Browserbase) -> None: extension = client.extensions.retrieve( "id", ) - assert_matches_type(ExtensionRetrieveResponse, extension, path=["response"]) + assert_matches_type(Extension, extension, path=["response"]) @parametrize def test_raw_response_retrieve(self, client: Browserbase) -> None: @@ -64,7 +64,7 @@ def test_raw_response_retrieve(self, client: Browserbase) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" extension = response.parse() - assert_matches_type(ExtensionRetrieveResponse, extension, path=["response"]) + assert_matches_type(Extension, extension, path=["response"]) @parametrize def test_streaming_response_retrieve(self, client: Browserbase) -> None: @@ -75,7 +75,7 @@ def test_streaming_response_retrieve(self, client: Browserbase) -> None: assert response.http_request.headers.get("X-Stainless-Lang") == "python" extension = response.parse() - assert_matches_type(ExtensionRetrieveResponse, extension, path=["response"]) + assert_matches_type(Extension, extension, path=["response"]) assert cast(Any, response.is_closed) is True @@ -135,7 +135,7 @@ async def test_method_create(self, async_client: AsyncBrowserbase) -> None: extension = await async_client.extensions.create( file=b"raw file contents", ) - assert_matches_type(ExtensionCreateResponse, extension, path=["response"]) + assert_matches_type(Extension, extension, path=["response"]) @parametrize async def test_raw_response_create(self, async_client: AsyncBrowserbase) -> None: @@ -146,7 +146,7 @@ async def test_raw_response_create(self, async_client: AsyncBrowserbase) -> None assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" extension = await response.parse() - assert_matches_type(ExtensionCreateResponse, extension, path=["response"]) + assert_matches_type(Extension, extension, path=["response"]) @parametrize async def test_streaming_response_create(self, async_client: AsyncBrowserbase) -> None: @@ -157,7 +157,7 @@ async def test_streaming_response_create(self, async_client: AsyncBrowserbase) - assert response.http_request.headers.get("X-Stainless-Lang") == "python" extension = await response.parse() - assert_matches_type(ExtensionCreateResponse, extension, path=["response"]) + assert_matches_type(Extension, extension, path=["response"]) assert cast(Any, response.is_closed) is True @@ -166,7 +166,7 @@ async def test_method_retrieve(self, async_client: AsyncBrowserbase) -> None: extension = await async_client.extensions.retrieve( "id", ) - assert_matches_type(ExtensionRetrieveResponse, extension, path=["response"]) + assert_matches_type(Extension, extension, path=["response"]) @parametrize async def test_raw_response_retrieve(self, async_client: AsyncBrowserbase) -> None: @@ -177,7 +177,7 @@ async def test_raw_response_retrieve(self, async_client: AsyncBrowserbase) -> No assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" extension = await response.parse() - assert_matches_type(ExtensionRetrieveResponse, extension, path=["response"]) + assert_matches_type(Extension, extension, path=["response"]) @parametrize async def test_streaming_response_retrieve(self, async_client: AsyncBrowserbase) -> None: @@ -188,7 +188,7 @@ async def test_streaming_response_retrieve(self, async_client: AsyncBrowserbase) assert response.http_request.headers.get("X-Stainless-Lang") == "python" extension = await response.parse() - assert_matches_type(ExtensionRetrieveResponse, extension, path=["response"]) + assert_matches_type(Extension, extension, path=["response"]) assert cast(Any, response.is_closed) is True diff --git a/tests/api_resources/test_projects.py b/tests/api_resources/test_projects.py index 0d8e3c94..c8241bf8 100644 --- a/tests/api_resources/test_projects.py +++ b/tests/api_resources/test_projects.py @@ -9,7 +9,7 @@ from browserbase import Browserbase, AsyncBrowserbase from tests.utils import assert_matches_type -from browserbase.types import ProjectListResponse, ProjectUsageResponse, ProjectRetrieveResponse +from browserbase.types import Project, ProjectUsage, ProjectListResponse base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") @@ -22,7 +22,7 @@ def test_method_retrieve(self, client: Browserbase) -> None: project = client.projects.retrieve( "id", ) - assert_matches_type(ProjectRetrieveResponse, project, path=["response"]) + assert_matches_type(Project, project, path=["response"]) @parametrize def test_raw_response_retrieve(self, client: Browserbase) -> None: @@ -33,7 +33,7 @@ def test_raw_response_retrieve(self, client: Browserbase) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" project = response.parse() - assert_matches_type(ProjectRetrieveResponse, project, path=["response"]) + assert_matches_type(Project, project, path=["response"]) @parametrize def test_streaming_response_retrieve(self, client: Browserbase) -> None: @@ -44,7 +44,7 @@ def test_streaming_response_retrieve(self, client: Browserbase) -> None: assert response.http_request.headers.get("X-Stainless-Lang") == "python" project = response.parse() - assert_matches_type(ProjectRetrieveResponse, project, path=["response"]) + assert_matches_type(Project, project, path=["response"]) assert cast(Any, response.is_closed) is True @@ -85,7 +85,7 @@ def test_method_usage(self, client: Browserbase) -> None: project = client.projects.usage( "id", ) - assert_matches_type(ProjectUsageResponse, project, path=["response"]) + assert_matches_type(ProjectUsage, project, path=["response"]) @parametrize def test_raw_response_usage(self, client: Browserbase) -> None: @@ -96,7 +96,7 @@ def test_raw_response_usage(self, client: Browserbase) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" project = response.parse() - assert_matches_type(ProjectUsageResponse, project, path=["response"]) + assert_matches_type(ProjectUsage, project, path=["response"]) @parametrize def test_streaming_response_usage(self, client: Browserbase) -> None: @@ -107,7 +107,7 @@ def test_streaming_response_usage(self, client: Browserbase) -> None: assert response.http_request.headers.get("X-Stainless-Lang") == "python" project = response.parse() - assert_matches_type(ProjectUsageResponse, project, path=["response"]) + assert_matches_type(ProjectUsage, project, path=["response"]) assert cast(Any, response.is_closed) is True @@ -129,7 +129,7 @@ async def test_method_retrieve(self, async_client: AsyncBrowserbase) -> None: project = await async_client.projects.retrieve( "id", ) - assert_matches_type(ProjectRetrieveResponse, project, path=["response"]) + assert_matches_type(Project, project, path=["response"]) @parametrize async def test_raw_response_retrieve(self, async_client: AsyncBrowserbase) -> None: @@ -140,7 +140,7 @@ async def test_raw_response_retrieve(self, async_client: AsyncBrowserbase) -> No assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" project = await response.parse() - assert_matches_type(ProjectRetrieveResponse, project, path=["response"]) + assert_matches_type(Project, project, path=["response"]) @parametrize async def test_streaming_response_retrieve(self, async_client: AsyncBrowserbase) -> None: @@ -151,7 +151,7 @@ async def test_streaming_response_retrieve(self, async_client: AsyncBrowserbase) assert response.http_request.headers.get("X-Stainless-Lang") == "python" project = await response.parse() - assert_matches_type(ProjectRetrieveResponse, project, path=["response"]) + assert_matches_type(Project, project, path=["response"]) assert cast(Any, response.is_closed) is True @@ -192,7 +192,7 @@ async def test_method_usage(self, async_client: AsyncBrowserbase) -> None: project = await async_client.projects.usage( "id", ) - assert_matches_type(ProjectUsageResponse, project, path=["response"]) + assert_matches_type(ProjectUsage, project, path=["response"]) @parametrize async def test_raw_response_usage(self, async_client: AsyncBrowserbase) -> None: @@ -203,7 +203,7 @@ async def test_raw_response_usage(self, async_client: AsyncBrowserbase) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" project = await response.parse() - assert_matches_type(ProjectUsageResponse, project, path=["response"]) + assert_matches_type(ProjectUsage, project, path=["response"]) @parametrize async def test_streaming_response_usage(self, async_client: AsyncBrowserbase) -> None: @@ -214,7 +214,7 @@ async def test_streaming_response_usage(self, async_client: AsyncBrowserbase) -> assert response.http_request.headers.get("X-Stainless-Lang") == "python" project = await response.parse() - assert_matches_type(ProjectUsageResponse, project, path=["response"]) + assert_matches_type(ProjectUsage, project, path=["response"]) assert cast(Any, response.is_closed) is True diff --git a/tests/api_resources/test_sessions.py b/tests/api_resources/test_sessions.py index 5a597678..d237069c 100644 --- a/tests/api_resources/test_sessions.py +++ b/tests/api_resources/test_sessions.py @@ -10,10 +10,10 @@ from browserbase import Browserbase, AsyncBrowserbase from tests.utils import assert_matches_type from browserbase.types import ( + Session, + SessionLiveURLs, SessionListResponse, - SessionDebugResponse, SessionCreateResponse, - SessionUpdateResponse, SessionRetrieveResponse, ) @@ -134,7 +134,7 @@ def test_method_update(self, client: Browserbase) -> None: id="id", status="REQUEST_RELEASE", ) - assert_matches_type(SessionUpdateResponse, session, path=["response"]) + assert_matches_type(Session, session, path=["response"]) @parametrize def test_method_update_with_all_params(self, client: Browserbase) -> None: @@ -143,7 +143,7 @@ def test_method_update_with_all_params(self, client: Browserbase) -> None: status="REQUEST_RELEASE", project_id="projectId", ) - assert_matches_type(SessionUpdateResponse, session, path=["response"]) + assert_matches_type(Session, session, path=["response"]) @parametrize def test_raw_response_update(self, client: Browserbase) -> None: @@ -155,7 +155,7 @@ def test_raw_response_update(self, client: Browserbase) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" session = response.parse() - assert_matches_type(SessionUpdateResponse, session, path=["response"]) + assert_matches_type(Session, session, path=["response"]) @parametrize def test_streaming_response_update(self, client: Browserbase) -> None: @@ -167,7 +167,7 @@ def test_streaming_response_update(self, client: Browserbase) -> None: assert response.http_request.headers.get("X-Stainless-Lang") == "python" session = response.parse() - assert_matches_type(SessionUpdateResponse, session, path=["response"]) + assert_matches_type(Session, session, path=["response"]) assert cast(Any, response.is_closed) is True @@ -217,7 +217,7 @@ def test_method_debug(self, client: Browserbase) -> None: session = client.sessions.debug( "id", ) - assert_matches_type(SessionDebugResponse, session, path=["response"]) + assert_matches_type(SessionLiveURLs, session, path=["response"]) @parametrize def test_raw_response_debug(self, client: Browserbase) -> None: @@ -228,7 +228,7 @@ def test_raw_response_debug(self, client: Browserbase) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" session = response.parse() - assert_matches_type(SessionDebugResponse, session, path=["response"]) + assert_matches_type(SessionLiveURLs, session, path=["response"]) @parametrize def test_streaming_response_debug(self, client: Browserbase) -> None: @@ -239,7 +239,7 @@ def test_streaming_response_debug(self, client: Browserbase) -> None: assert response.http_request.headers.get("X-Stainless-Lang") == "python" session = response.parse() - assert_matches_type(SessionDebugResponse, session, path=["response"]) + assert_matches_type(SessionLiveURLs, session, path=["response"]) assert cast(Any, response.is_closed) is True @@ -367,7 +367,7 @@ async def test_method_update(self, async_client: AsyncBrowserbase) -> None: id="id", status="REQUEST_RELEASE", ) - assert_matches_type(SessionUpdateResponse, session, path=["response"]) + assert_matches_type(Session, session, path=["response"]) @parametrize async def test_method_update_with_all_params(self, async_client: AsyncBrowserbase) -> None: @@ -376,7 +376,7 @@ async def test_method_update_with_all_params(self, async_client: AsyncBrowserbas status="REQUEST_RELEASE", project_id="projectId", ) - assert_matches_type(SessionUpdateResponse, session, path=["response"]) + assert_matches_type(Session, session, path=["response"]) @parametrize async def test_raw_response_update(self, async_client: AsyncBrowserbase) -> None: @@ -388,7 +388,7 @@ async def test_raw_response_update(self, async_client: AsyncBrowserbase) -> None assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" session = await response.parse() - assert_matches_type(SessionUpdateResponse, session, path=["response"]) + assert_matches_type(Session, session, path=["response"]) @parametrize async def test_streaming_response_update(self, async_client: AsyncBrowserbase) -> None: @@ -400,7 +400,7 @@ async def test_streaming_response_update(self, async_client: AsyncBrowserbase) - assert response.http_request.headers.get("X-Stainless-Lang") == "python" session = await response.parse() - assert_matches_type(SessionUpdateResponse, session, path=["response"]) + assert_matches_type(Session, session, path=["response"]) assert cast(Any, response.is_closed) is True @@ -450,7 +450,7 @@ async def test_method_debug(self, async_client: AsyncBrowserbase) -> None: session = await async_client.sessions.debug( "id", ) - assert_matches_type(SessionDebugResponse, session, path=["response"]) + assert_matches_type(SessionLiveURLs, session, path=["response"]) @parametrize async def test_raw_response_debug(self, async_client: AsyncBrowserbase) -> None: @@ -461,7 +461,7 @@ async def test_raw_response_debug(self, async_client: AsyncBrowserbase) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" session = await response.parse() - assert_matches_type(SessionDebugResponse, session, path=["response"]) + assert_matches_type(SessionLiveURLs, session, path=["response"]) @parametrize async def test_streaming_response_debug(self, async_client: AsyncBrowserbase) -> None: @@ -472,7 +472,7 @@ async def test_streaming_response_debug(self, async_client: AsyncBrowserbase) -> assert response.http_request.headers.get("X-Stainless-Lang") == "python" session = await response.parse() - assert_matches_type(SessionDebugResponse, session, path=["response"]) + assert_matches_type(SessionLiveURLs, session, path=["response"]) assert cast(Any, response.is_closed) is True From 8c51c5b9fd506ca11d70440b32c36496a102ae01 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 26 Feb 2026 19:06:56 +0000 Subject: [PATCH 257/330] codegen metadata --- .stats.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index 311b5669..05de0332 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 19 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fbrowserbase-2cab80bfbbe55ea230b095f214f314e08d30fd5f9cf21781a09dc2925934886a.yml +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fbrowserbase-215bc4361122162181eecce83c0dbdda7c45a21801e7addb75102e8011413069.yml openapi_spec_hash: c4fadc5bb6b84cd3988c8d864b67bf61 -config_hash: b01d72cbe03bd762a73b05744086b2ec +config_hash: a106b247c7cdf02ac1033077402cfe2d From b21125e49436c9b1fff0216e3e2b7685ae3d8c1d Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 3 Mar 2026 22:46:01 +0000 Subject: [PATCH 258/330] chore(internal): version bump --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/browserbase/_version.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 3d362d5e..70fc11c6 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "1.5.0-alpha.1" + ".": "1.5.0-alpha.2" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 340f3b38..5331c5e1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "browserbase" -version = "1.5.0-alpha.1" +version = "1.5.0-alpha.2" description = "The official Python library for the Browserbase API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/browserbase/_version.py b/src/browserbase/_version.py index 6fa8f70b..afe412cf 100644 --- a/src/browserbase/_version.py +++ b/src/browserbase/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "browserbase" -__version__ = "1.5.0-alpha.1" # x-release-please-version +__version__ = "1.5.0-alpha.2" # x-release-please-version From 8dcf93c6b81a262e23eb68033f6e27847a0f4856 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 6 Mar 2026 21:44:29 +0000 Subject: [PATCH 259/330] chore(test): do not count install time for mock server timeout --- scripts/mock | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/scripts/mock b/scripts/mock index 0b28f6ea..bcf3b392 100755 --- a/scripts/mock +++ b/scripts/mock @@ -21,11 +21,22 @@ echo "==> Starting mock server with URL ${URL}" # Run prism mock on the given spec if [ "$1" == "--daemon" ]; then + # Pre-install the package so the download doesn't eat into the startup timeout + npm exec --package=@stainless-api/prism-cli@5.15.0 -- prism --version + npm exec --package=@stainless-api/prism-cli@5.15.0 -- prism mock "$URL" &> .prism.log & - # Wait for server to come online + # Wait for server to come online (max 30s) echo -n "Waiting for server" + attempts=0 while ! grep -q "✖ fatal\|Prism is listening" ".prism.log" ; do + attempts=$((attempts + 1)) + if [ "$attempts" -ge 300 ]; then + echo + echo "Timed out waiting for Prism server to start" + cat .prism.log + exit 1 + fi echo -n "." sleep 0.1 done From a48fb66a62cd2bcafb5f215e29b487ed3b8f145c Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 7 Mar 2026 21:25:41 +0000 Subject: [PATCH 260/330] chore(ci): skip uploading artifacts on stainless-internal branches --- .github/workflows/ci.yml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c77d6d73..0925562d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -61,14 +61,18 @@ jobs: run: rye build - name: Get GitHub OIDC Token - if: github.repository == 'stainless-sdks/browserbase-python' + if: |- + github.repository == 'stainless-sdks/browserbase-python' && + !startsWith(github.ref, 'refs/heads/stl/') id: github-oidc uses: actions/github-script@v8 with: script: core.setOutput('github_token', await core.getIDToken()); - name: Upload tarball - if: github.repository == 'stainless-sdks/browserbase-python' + if: |- + github.repository == 'stainless-sdks/browserbase-python' && + !startsWith(github.ref, 'refs/heads/stl/') env: URL: https://pkg.stainless.com/s AUTH: ${{ steps.github-oidc.outputs.github_token }} From 11ced31553ba9c896a9a99fcecd6d4aa33326f4e Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 7 Mar 2026 21:29:18 +0000 Subject: [PATCH 261/330] chore: update placeholder string --- tests/api_resources/sessions/test_uploads.py | 16 ++++++++-------- tests/api_resources/test_extensions.py | 12 ++++++------ 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/tests/api_resources/sessions/test_uploads.py b/tests/api_resources/sessions/test_uploads.py index 748b92e7..b98dc0a5 100644 --- a/tests/api_resources/sessions/test_uploads.py +++ b/tests/api_resources/sessions/test_uploads.py @@ -21,7 +21,7 @@ class TestUploads: def test_method_create(self, client: Browserbase) -> None: upload = client.sessions.uploads.create( id="id", - file=b"raw file contents", + file=b"Example data", ) assert_matches_type(UploadCreateResponse, upload, path=["response"]) @@ -29,7 +29,7 @@ def test_method_create(self, client: Browserbase) -> None: def test_raw_response_create(self, client: Browserbase) -> None: response = client.sessions.uploads.with_raw_response.create( id="id", - file=b"raw file contents", + file=b"Example data", ) assert response.is_closed is True @@ -41,7 +41,7 @@ def test_raw_response_create(self, client: Browserbase) -> None: def test_streaming_response_create(self, client: Browserbase) -> None: with client.sessions.uploads.with_streaming_response.create( id="id", - file=b"raw file contents", + file=b"Example data", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -56,7 +56,7 @@ def test_path_params_create(self, client: Browserbase) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): client.sessions.uploads.with_raw_response.create( id="", - file=b"raw file contents", + file=b"Example data", ) @@ -69,7 +69,7 @@ class TestAsyncUploads: async def test_method_create(self, async_client: AsyncBrowserbase) -> None: upload = await async_client.sessions.uploads.create( id="id", - file=b"raw file contents", + file=b"Example data", ) assert_matches_type(UploadCreateResponse, upload, path=["response"]) @@ -77,7 +77,7 @@ async def test_method_create(self, async_client: AsyncBrowserbase) -> None: async def test_raw_response_create(self, async_client: AsyncBrowserbase) -> None: response = await async_client.sessions.uploads.with_raw_response.create( id="id", - file=b"raw file contents", + file=b"Example data", ) assert response.is_closed is True @@ -89,7 +89,7 @@ async def test_raw_response_create(self, async_client: AsyncBrowserbase) -> None async def test_streaming_response_create(self, async_client: AsyncBrowserbase) -> None: async with async_client.sessions.uploads.with_streaming_response.create( id="id", - file=b"raw file contents", + file=b"Example data", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -104,5 +104,5 @@ async def test_path_params_create(self, async_client: AsyncBrowserbase) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): await async_client.sessions.uploads.with_raw_response.create( id="", - file=b"raw file contents", + file=b"Example data", ) diff --git a/tests/api_resources/test_extensions.py b/tests/api_resources/test_extensions.py index 6b6a0183..b6b6260b 100644 --- a/tests/api_resources/test_extensions.py +++ b/tests/api_resources/test_extensions.py @@ -20,14 +20,14 @@ class TestExtensions: @parametrize def test_method_create(self, client: Browserbase) -> None: extension = client.extensions.create( - file=b"raw file contents", + file=b"Example data", ) assert_matches_type(Extension, extension, path=["response"]) @parametrize def test_raw_response_create(self, client: Browserbase) -> None: response = client.extensions.with_raw_response.create( - file=b"raw file contents", + file=b"Example data", ) assert response.is_closed is True @@ -38,7 +38,7 @@ def test_raw_response_create(self, client: Browserbase) -> None: @parametrize def test_streaming_response_create(self, client: Browserbase) -> None: with client.extensions.with_streaming_response.create( - file=b"raw file contents", + file=b"Example data", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -133,14 +133,14 @@ class TestAsyncExtensions: @parametrize async def test_method_create(self, async_client: AsyncBrowserbase) -> None: extension = await async_client.extensions.create( - file=b"raw file contents", + file=b"Example data", ) assert_matches_type(Extension, extension, path=["response"]) @parametrize async def test_raw_response_create(self, async_client: AsyncBrowserbase) -> None: response = await async_client.extensions.with_raw_response.create( - file=b"raw file contents", + file=b"Example data", ) assert response.is_closed is True @@ -151,7 +151,7 @@ async def test_raw_response_create(self, async_client: AsyncBrowserbase) -> None @parametrize async def test_streaming_response_create(self, async_client: AsyncBrowserbase) -> None: async with async_client.extensions.with_streaming_response.create( - file=b"raw file contents", + file=b"Example data", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" From 519c1c5e551e3564319d7b6596ca4efc1dfefad7 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 11 Mar 2026 19:13:39 +0000 Subject: [PATCH 262/330] feat: [CORE-1804][apps/api] Add fetch API schema --- .stats.yml | 8 +- api.md | 12 ++ src/browserbase/_client.py | 39 +++- src/browserbase/resources/__init__.py | 14 ++ src/browserbase/resources/fetch_api.py | 201 ++++++++++++++++++ src/browserbase/types/__init__.py | 2 + .../types/fetch_api_create_params.py | 23 ++ .../types/fetch_api_create_response.py | 31 +++ tests/api_resources/test_fetch_api.py | 106 +++++++++ 9 files changed, 431 insertions(+), 5 deletions(-) create mode 100644 src/browserbase/resources/fetch_api.py create mode 100644 src/browserbase/types/fetch_api_create_params.py create mode 100644 src/browserbase/types/fetch_api_create_response.py create mode 100644 tests/api_resources/test_fetch_api.py diff --git a/.stats.yml b/.stats.yml index 05de0332..3aabff9c 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ -configured_endpoints: 19 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fbrowserbase-215bc4361122162181eecce83c0dbdda7c45a21801e7addb75102e8011413069.yml -openapi_spec_hash: c4fadc5bb6b84cd3988c8d864b67bf61 -config_hash: a106b247c7cdf02ac1033077402cfe2d +configured_endpoints: 20 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fbrowserbase-b20f9fea14d79990ab1af3d276f931e026cd955ac623ec6ace80b2af90de170f.yml +openapi_spec_hash: 943ff4b3297014503fdc9854544cb9a4 +config_hash: 55c54fdafc9e80be584829b5724b00ab diff --git a/api.md b/api.md index d2d26e57..ac45583b 100644 --- a/api.md +++ b/api.md @@ -27,6 +27,18 @@ Methods: - client.extensions.retrieve(id) -> Extension - client.extensions.delete(id) -> None +# FetchAPI + +Types: + +```python +from browserbase.types import FetchAPICreateResponse +``` + +Methods: + +- client.fetch_api.create(\*\*params) -> FetchAPICreateResponse + # Projects Types: diff --git a/src/browserbase/_client.py b/src/browserbase/_client.py index 5bb997a7..70a7b71d 100644 --- a/src/browserbase/_client.py +++ b/src/browserbase/_client.py @@ -31,9 +31,10 @@ ) if TYPE_CHECKING: - from .resources import contexts, projects, sessions, extensions + from .resources import contexts, projects, sessions, fetch_api, extensions from .resources.contexts import ContextsResource, AsyncContextsResource from .resources.projects import ProjectsResource, AsyncProjectsResource + from .resources.fetch_api import FetchAPIResource, AsyncFetchAPIResource from .resources.extensions import ExtensionsResource, AsyncExtensionsResource from .resources.sessions.sessions import SessionsResource, AsyncSessionsResource @@ -116,6 +117,12 @@ def extensions(self) -> ExtensionsResource: return ExtensionsResource(self) + @cached_property + def fetch_api(self) -> FetchAPIResource: + from .resources.fetch_api import FetchAPIResource + + return FetchAPIResource(self) + @cached_property def projects(self) -> ProjectsResource: from .resources.projects import ProjectsResource @@ -308,6 +315,12 @@ def extensions(self) -> AsyncExtensionsResource: return AsyncExtensionsResource(self) + @cached_property + def fetch_api(self) -> AsyncFetchAPIResource: + from .resources.fetch_api import AsyncFetchAPIResource + + return AsyncFetchAPIResource(self) + @cached_property def projects(self) -> AsyncProjectsResource: from .resources.projects import AsyncProjectsResource @@ -451,6 +464,12 @@ def extensions(self) -> extensions.ExtensionsResourceWithRawResponse: return ExtensionsResourceWithRawResponse(self._client.extensions) + @cached_property + def fetch_api(self) -> fetch_api.FetchAPIResourceWithRawResponse: + from .resources.fetch_api import FetchAPIResourceWithRawResponse + + return FetchAPIResourceWithRawResponse(self._client.fetch_api) + @cached_property def projects(self) -> projects.ProjectsResourceWithRawResponse: from .resources.projects import ProjectsResourceWithRawResponse @@ -482,6 +501,12 @@ def extensions(self) -> extensions.AsyncExtensionsResourceWithRawResponse: return AsyncExtensionsResourceWithRawResponse(self._client.extensions) + @cached_property + def fetch_api(self) -> fetch_api.AsyncFetchAPIResourceWithRawResponse: + from .resources.fetch_api import AsyncFetchAPIResourceWithRawResponse + + return AsyncFetchAPIResourceWithRawResponse(self._client.fetch_api) + @cached_property def projects(self) -> projects.AsyncProjectsResourceWithRawResponse: from .resources.projects import AsyncProjectsResourceWithRawResponse @@ -513,6 +538,12 @@ def extensions(self) -> extensions.ExtensionsResourceWithStreamingResponse: return ExtensionsResourceWithStreamingResponse(self._client.extensions) + @cached_property + def fetch_api(self) -> fetch_api.FetchAPIResourceWithStreamingResponse: + from .resources.fetch_api import FetchAPIResourceWithStreamingResponse + + return FetchAPIResourceWithStreamingResponse(self._client.fetch_api) + @cached_property def projects(self) -> projects.ProjectsResourceWithStreamingResponse: from .resources.projects import ProjectsResourceWithStreamingResponse @@ -544,6 +575,12 @@ def extensions(self) -> extensions.AsyncExtensionsResourceWithStreamingResponse: return AsyncExtensionsResourceWithStreamingResponse(self._client.extensions) + @cached_property + def fetch_api(self) -> fetch_api.AsyncFetchAPIResourceWithStreamingResponse: + from .resources.fetch_api import AsyncFetchAPIResourceWithStreamingResponse + + return AsyncFetchAPIResourceWithStreamingResponse(self._client.fetch_api) + @cached_property def projects(self) -> projects.AsyncProjectsResourceWithStreamingResponse: from .resources.projects import AsyncProjectsResourceWithStreamingResponse diff --git a/src/browserbase/resources/__init__.py b/src/browserbase/resources/__init__.py index 73451a50..f5a2bf0c 100644 --- a/src/browserbase/resources/__init__.py +++ b/src/browserbase/resources/__init__.py @@ -24,6 +24,14 @@ SessionsResourceWithStreamingResponse, AsyncSessionsResourceWithStreamingResponse, ) +from .fetch_api import ( + FetchAPIResource, + AsyncFetchAPIResource, + FetchAPIResourceWithRawResponse, + AsyncFetchAPIResourceWithRawResponse, + FetchAPIResourceWithStreamingResponse, + AsyncFetchAPIResourceWithStreamingResponse, +) from .extensions import ( ExtensionsResource, AsyncExtensionsResource, @@ -46,6 +54,12 @@ "AsyncExtensionsResourceWithRawResponse", "ExtensionsResourceWithStreamingResponse", "AsyncExtensionsResourceWithStreamingResponse", + "FetchAPIResource", + "AsyncFetchAPIResource", + "FetchAPIResourceWithRawResponse", + "AsyncFetchAPIResourceWithRawResponse", + "FetchAPIResourceWithStreamingResponse", + "AsyncFetchAPIResourceWithStreamingResponse", "ProjectsResource", "AsyncProjectsResource", "ProjectsResourceWithRawResponse", diff --git a/src/browserbase/resources/fetch_api.py b/src/browserbase/resources/fetch_api.py new file mode 100644 index 00000000..dc016722 --- /dev/null +++ b/src/browserbase/resources/fetch_api.py @@ -0,0 +1,201 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import httpx + +from ..types import fetch_api_create_params +from .._types import Body, Omit, Query, Headers, NotGiven, omit, not_given +from .._utils import maybe_transform, 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.fetch_api_create_response import FetchAPICreateResponse + +__all__ = ["FetchAPIResource", "AsyncFetchAPIResource"] + + +class FetchAPIResource(SyncAPIResource): + @cached_property + def with_raw_response(self) -> FetchAPIResourceWithRawResponse: + """ + 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/sdk-python#accessing-raw-response-data-eg-headers + """ + return FetchAPIResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> FetchAPIResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/browserbase/sdk-python#with_streaming_response + """ + return FetchAPIResourceWithStreamingResponse(self) + + def create( + self, + *, + url: str, + allow_insecure_ssl: bool | Omit = omit, + allow_redirects: bool | Omit = omit, + proxies: bool | 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, + ) -> FetchAPICreateResponse: + """ + Fetch a page and return its content, headers, and metadata. + + Args: + url: The URL to fetch + + allow_insecure_ssl: Whether to bypass TLS certificate verification + + allow_redirects: Whether to follow HTTP redirects + + proxies: Whether to enable proxy support for the request + + 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( + "/v1/fetch", + body=maybe_transform( + { + "url": url, + "allow_insecure_ssl": allow_insecure_ssl, + "allow_redirects": allow_redirects, + "proxies": proxies, + }, + fetch_api_create_params.FetchAPICreateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=FetchAPICreateResponse, + ) + + +class AsyncFetchAPIResource(AsyncAPIResource): + @cached_property + def with_raw_response(self) -> AsyncFetchAPIResourceWithRawResponse: + """ + 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/sdk-python#accessing-raw-response-data-eg-headers + """ + return AsyncFetchAPIResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncFetchAPIResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/browserbase/sdk-python#with_streaming_response + """ + return AsyncFetchAPIResourceWithStreamingResponse(self) + + async def create( + self, + *, + url: str, + allow_insecure_ssl: bool | Omit = omit, + allow_redirects: bool | Omit = omit, + proxies: bool | 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, + ) -> FetchAPICreateResponse: + """ + Fetch a page and return its content, headers, and metadata. + + Args: + url: The URL to fetch + + allow_insecure_ssl: Whether to bypass TLS certificate verification + + allow_redirects: Whether to follow HTTP redirects + + proxies: Whether to enable proxy support for the request + + 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( + "/v1/fetch", + body=await async_maybe_transform( + { + "url": url, + "allow_insecure_ssl": allow_insecure_ssl, + "allow_redirects": allow_redirects, + "proxies": proxies, + }, + fetch_api_create_params.FetchAPICreateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=FetchAPICreateResponse, + ) + + +class FetchAPIResourceWithRawResponse: + def __init__(self, fetch_api: FetchAPIResource) -> None: + self._fetch_api = fetch_api + + self.create = to_raw_response_wrapper( + fetch_api.create, + ) + + +class AsyncFetchAPIResourceWithRawResponse: + def __init__(self, fetch_api: AsyncFetchAPIResource) -> None: + self._fetch_api = fetch_api + + self.create = async_to_raw_response_wrapper( + fetch_api.create, + ) + + +class FetchAPIResourceWithStreamingResponse: + def __init__(self, fetch_api: FetchAPIResource) -> None: + self._fetch_api = fetch_api + + self.create = to_streamed_response_wrapper( + fetch_api.create, + ) + + +class AsyncFetchAPIResourceWithStreamingResponse: + def __init__(self, fetch_api: AsyncFetchAPIResource) -> None: + self._fetch_api = fetch_api + + self.create = async_to_streamed_response_wrapper( + fetch_api.create, + ) diff --git a/src/browserbase/types/__init__.py b/src/browserbase/types/__init__.py index 4dd85ddb..52659907 100644 --- a/src/browserbase/types/__init__.py +++ b/src/browserbase/types/__init__.py @@ -17,5 +17,7 @@ from .context_create_response import ContextCreateResponse as ContextCreateResponse from .context_update_response import ContextUpdateResponse as ContextUpdateResponse from .extension_create_params import ExtensionCreateParams as ExtensionCreateParams +from .fetch_api_create_params import FetchAPICreateParams as FetchAPICreateParams from .session_create_response import SessionCreateResponse as SessionCreateResponse +from .fetch_api_create_response import FetchAPICreateResponse as FetchAPICreateResponse from .session_retrieve_response import SessionRetrieveResponse as SessionRetrieveResponse diff --git a/src/browserbase/types/fetch_api_create_params.py b/src/browserbase/types/fetch_api_create_params.py new file mode 100644 index 00000000..84a8a052 --- /dev/null +++ b/src/browserbase/types/fetch_api_create_params.py @@ -0,0 +1,23 @@ +# 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 .._utils import PropertyInfo + +__all__ = ["FetchAPICreateParams"] + + +class FetchAPICreateParams(TypedDict, total=False): + url: Required[str] + """The URL to fetch""" + + allow_insecure_ssl: Annotated[bool, PropertyInfo(alias="allowInsecureSsl")] + """Whether to bypass TLS certificate verification""" + + allow_redirects: Annotated[bool, PropertyInfo(alias="allowRedirects")] + """Whether to follow HTTP redirects""" + + proxies: bool + """Whether to enable proxy support for the request""" diff --git a/src/browserbase/types/fetch_api_create_response.py b/src/browserbase/types/fetch_api_create_response.py new file mode 100644 index 00000000..cfafbba3 --- /dev/null +++ b/src/browserbase/types/fetch_api_create_response.py @@ -0,0 +1,31 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Dict + +from pydantic import Field as FieldInfo + +from .._models import BaseModel + +__all__ = ["FetchAPICreateResponse"] + + +class FetchAPICreateResponse(BaseModel): + """Response body for fetch""" + + id: str + """Unique identifier for the fetch request""" + + content: str + """The response body content""" + + content_type: str = FieldInfo(alias="contentType") + """The MIME type of the response""" + + encoding: str + """The character encoding of the response""" + + headers: Dict[str, str] + """Response headers as key-value pairs""" + + status_code: int = FieldInfo(alias="statusCode") + """HTTP status code of the fetched response""" diff --git a/tests/api_resources/test_fetch_api.py b/tests/api_resources/test_fetch_api.py new file mode 100644 index 00000000..b9a0455b --- /dev/null +++ b/tests/api_resources/test_fetch_api.py @@ -0,0 +1,106 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import os +from typing import Any, cast + +import pytest + +from browserbase import Browserbase, AsyncBrowserbase +from tests.utils import assert_matches_type +from browserbase.types import FetchAPICreateResponse + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + + +class TestFetchAPI: + parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) + + @parametrize + def test_method_create(self, client: Browserbase) -> None: + fetch_api = client.fetch_api.create( + url="https://example.com", + ) + assert_matches_type(FetchAPICreateResponse, fetch_api, path=["response"]) + + @parametrize + def test_method_create_with_all_params(self, client: Browserbase) -> None: + fetch_api = client.fetch_api.create( + url="https://example.com", + allow_insecure_ssl=True, + allow_redirects=True, + proxies=True, + ) + assert_matches_type(FetchAPICreateResponse, fetch_api, path=["response"]) + + @parametrize + def test_raw_response_create(self, client: Browserbase) -> None: + response = client.fetch_api.with_raw_response.create( + url="https://example.com", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + fetch_api = response.parse() + assert_matches_type(FetchAPICreateResponse, fetch_api, path=["response"]) + + @parametrize + def test_streaming_response_create(self, client: Browserbase) -> None: + with client.fetch_api.with_streaming_response.create( + url="https://example.com", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + fetch_api = response.parse() + assert_matches_type(FetchAPICreateResponse, fetch_api, path=["response"]) + + assert cast(Any, response.is_closed) is True + + +class TestAsyncFetchAPI: + parametrize = pytest.mark.parametrize( + "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] + ) + + @parametrize + async def test_method_create(self, async_client: AsyncBrowserbase) -> None: + fetch_api = await async_client.fetch_api.create( + url="https://example.com", + ) + assert_matches_type(FetchAPICreateResponse, fetch_api, path=["response"]) + + @parametrize + async def test_method_create_with_all_params(self, async_client: AsyncBrowserbase) -> None: + fetch_api = await async_client.fetch_api.create( + url="https://example.com", + allow_insecure_ssl=True, + allow_redirects=True, + proxies=True, + ) + assert_matches_type(FetchAPICreateResponse, fetch_api, path=["response"]) + + @parametrize + async def test_raw_response_create(self, async_client: AsyncBrowserbase) -> None: + response = await async_client.fetch_api.with_raw_response.create( + url="https://example.com", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + fetch_api = await response.parse() + assert_matches_type(FetchAPICreateResponse, fetch_api, path=["response"]) + + @parametrize + async def test_streaming_response_create(self, async_client: AsyncBrowserbase) -> None: + async with async_client.fetch_api.with_streaming_response.create( + url="https://example.com", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + fetch_api = await response.parse() + assert_matches_type(FetchAPICreateResponse, fetch_api, path=["response"]) + + assert cast(Any, response.is_closed) is True From c4c3575634abdac857242b561a43a45c9df15039 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 11 Mar 2026 19:43:59 +0000 Subject: [PATCH 263/330] chore(internal): version bump --- .release-please-manifest.json | 2 +- README.md | 4 ++-- pyproject.toml | 2 +- src/browserbase/_version.py | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 70fc11c6..7deae338 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "1.5.0-alpha.2" + ".": "1.6.0" } \ No newline at end of file diff --git a/README.md b/README.md index 89c681fa..42bfedbf 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ The REST API documentation can be found on [docs.browserbase.com](https://docs.b ```sh # install from PyPI -pip install '--pre browserbase' +pip install browserbase ``` ## Usage @@ -75,7 +75,7 @@ You can enable this by installing `aiohttp`: ```sh # install from PyPI -pip install '--pre browserbase[aiohttp]' +pip install browserbase[aiohttp] ``` Then you can enable it by instantiating the client with `http_client=DefaultAioHttpClient()`: diff --git a/pyproject.toml b/pyproject.toml index 5331c5e1..b72a7ccb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "browserbase" -version = "1.5.0-alpha.2" +version = "1.6.0" description = "The official Python library for the Browserbase API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/browserbase/_version.py b/src/browserbase/_version.py index afe412cf..a7f6a7d4 100644 --- a/src/browserbase/_version.py +++ b/src/browserbase/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "browserbase" -__version__ = "1.5.0-alpha.2" # x-release-please-version +__version__ = "1.6.0" # x-release-please-version From 6aeca08c0947bc0eabcbda23cb4a792282848454 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 11 Mar 2026 21:50:13 +0000 Subject: [PATCH 264/330] feat: [CORE-1796][apps/api] Update Node.js SDK --- .stats.yml | 4 ++-- src/browserbase/types/fetch_api_create_response.py | 2 -- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/.stats.yml b/.stats.yml index 3aabff9c..5db26811 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 20 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fbrowserbase-b20f9fea14d79990ab1af3d276f931e026cd955ac623ec6ace80b2af90de170f.yml -openapi_spec_hash: 943ff4b3297014503fdc9854544cb9a4 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fbrowserbase-d5413d9ed31cc9a28a804736cedc95e8171a9465ed66583ae67f225e9c739ca2.yml +openapi_spec_hash: fafc55851f83f26ddb2554589965e8f5 config_hash: 55c54fdafc9e80be584829b5724b00ab diff --git a/src/browserbase/types/fetch_api_create_response.py b/src/browserbase/types/fetch_api_create_response.py index cfafbba3..f97f5635 100644 --- a/src/browserbase/types/fetch_api_create_response.py +++ b/src/browserbase/types/fetch_api_create_response.py @@ -10,8 +10,6 @@ class FetchAPICreateResponse(BaseModel): - """Response body for fetch""" - id: str """Unique identifier for the fetch request""" From f9464c55a97a45681233cd65c47f028ef7d4e23b Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 16 Mar 2026 20:38:41 +0000 Subject: [PATCH 265/330] feat: [GRO-000] docs: Add guide + docs for search api --- .stats.yml | 8 +- api.md | 12 ++ src/browserbase/_client.py | 39 +++- src/browserbase/resources/__init__.py | 14 ++ src/browserbase/resources/search.py | 185 +++++++++++++++++++ src/browserbase/types/__init__.py | 2 + src/browserbase/types/search_web_params.py | 17 ++ src/browserbase/types/search_web_response.py | 46 +++++ tests/api_resources/test_search.py | 102 ++++++++++ 9 files changed, 420 insertions(+), 5 deletions(-) create mode 100644 src/browserbase/resources/search.py create mode 100644 src/browserbase/types/search_web_params.py create mode 100644 src/browserbase/types/search_web_response.py create mode 100644 tests/api_resources/test_search.py diff --git a/.stats.yml b/.stats.yml index 5db26811..28a1de1a 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ -configured_endpoints: 20 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fbrowserbase-d5413d9ed31cc9a28a804736cedc95e8171a9465ed66583ae67f225e9c739ca2.yml -openapi_spec_hash: fafc55851f83f26ddb2554589965e8f5 -config_hash: 55c54fdafc9e80be584829b5724b00ab +configured_endpoints: 21 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fbrowserbase-9b1e2a2abf39dd780601935a9a9ee04cb939e2c3ba76627535f625b6aeaf5eb7.yml +openapi_spec_hash: 12fe5f4306c43fdfb394a33f79391a82 +config_hash: cf04ecfb8dad5fbd8b85be25d6e9ec55 diff --git a/api.md b/api.md index ac45583b..b6066cb8 100644 --- a/api.md +++ b/api.md @@ -53,6 +53,18 @@ Methods: - client.projects.list() -> ProjectListResponse - client.projects.usage(id) -> ProjectUsage +# Search + +Types: + +```python +from browserbase.types import SearchWebResponse +``` + +Methods: + +- client.search.web(\*\*params) -> SearchWebResponse + # Sessions Types: diff --git a/src/browserbase/_client.py b/src/browserbase/_client.py index 70a7b71d..d642273a 100644 --- a/src/browserbase/_client.py +++ b/src/browserbase/_client.py @@ -31,7 +31,8 @@ ) if TYPE_CHECKING: - from .resources import contexts, projects, sessions, fetch_api, extensions + from .resources import search, contexts, projects, sessions, fetch_api, extensions + from .resources.search import SearchResource, AsyncSearchResource from .resources.contexts import ContextsResource, AsyncContextsResource from .resources.projects import ProjectsResource, AsyncProjectsResource from .resources.fetch_api import FetchAPIResource, AsyncFetchAPIResource @@ -129,6 +130,12 @@ def projects(self) -> ProjectsResource: return ProjectsResource(self) + @cached_property + def search(self) -> SearchResource: + from .resources.search import SearchResource + + return SearchResource(self) + @cached_property def sessions(self) -> SessionsResource: from .resources.sessions import SessionsResource @@ -327,6 +334,12 @@ def projects(self) -> AsyncProjectsResource: return AsyncProjectsResource(self) + @cached_property + def search(self) -> AsyncSearchResource: + from .resources.search import AsyncSearchResource + + return AsyncSearchResource(self) + @cached_property def sessions(self) -> AsyncSessionsResource: from .resources.sessions import AsyncSessionsResource @@ -476,6 +489,12 @@ def projects(self) -> projects.ProjectsResourceWithRawResponse: return ProjectsResourceWithRawResponse(self._client.projects) + @cached_property + def search(self) -> search.SearchResourceWithRawResponse: + from .resources.search import SearchResourceWithRawResponse + + return SearchResourceWithRawResponse(self._client.search) + @cached_property def sessions(self) -> sessions.SessionsResourceWithRawResponse: from .resources.sessions import SessionsResourceWithRawResponse @@ -513,6 +532,12 @@ def projects(self) -> projects.AsyncProjectsResourceWithRawResponse: return AsyncProjectsResourceWithRawResponse(self._client.projects) + @cached_property + def search(self) -> search.AsyncSearchResourceWithRawResponse: + from .resources.search import AsyncSearchResourceWithRawResponse + + return AsyncSearchResourceWithRawResponse(self._client.search) + @cached_property def sessions(self) -> sessions.AsyncSessionsResourceWithRawResponse: from .resources.sessions import AsyncSessionsResourceWithRawResponse @@ -550,6 +575,12 @@ def projects(self) -> projects.ProjectsResourceWithStreamingResponse: return ProjectsResourceWithStreamingResponse(self._client.projects) + @cached_property + def search(self) -> search.SearchResourceWithStreamingResponse: + from .resources.search import SearchResourceWithStreamingResponse + + return SearchResourceWithStreamingResponse(self._client.search) + @cached_property def sessions(self) -> sessions.SessionsResourceWithStreamingResponse: from .resources.sessions import SessionsResourceWithStreamingResponse @@ -587,6 +618,12 @@ def projects(self) -> projects.AsyncProjectsResourceWithStreamingResponse: return AsyncProjectsResourceWithStreamingResponse(self._client.projects) + @cached_property + def search(self) -> search.AsyncSearchResourceWithStreamingResponse: + from .resources.search import AsyncSearchResourceWithStreamingResponse + + return AsyncSearchResourceWithStreamingResponse(self._client.search) + @cached_property def sessions(self) -> sessions.AsyncSessionsResourceWithStreamingResponse: from .resources.sessions import AsyncSessionsResourceWithStreamingResponse diff --git a/src/browserbase/resources/__init__.py b/src/browserbase/resources/__init__.py index f5a2bf0c..83a8788d 100644 --- a/src/browserbase/resources/__init__.py +++ b/src/browserbase/resources/__init__.py @@ -1,5 +1,13 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. +from .search import ( + SearchResource, + AsyncSearchResource, + SearchResourceWithRawResponse, + AsyncSearchResourceWithRawResponse, + SearchResourceWithStreamingResponse, + AsyncSearchResourceWithStreamingResponse, +) from .contexts import ( ContextsResource, AsyncContextsResource, @@ -66,6 +74,12 @@ "AsyncProjectsResourceWithRawResponse", "ProjectsResourceWithStreamingResponse", "AsyncProjectsResourceWithStreamingResponse", + "SearchResource", + "AsyncSearchResource", + "SearchResourceWithRawResponse", + "AsyncSearchResourceWithRawResponse", + "SearchResourceWithStreamingResponse", + "AsyncSearchResourceWithStreamingResponse", "SessionsResource", "AsyncSessionsResource", "SessionsResourceWithRawResponse", diff --git a/src/browserbase/resources/search.py b/src/browserbase/resources/search.py new file mode 100644 index 00000000..87f34e60 --- /dev/null +++ b/src/browserbase/resources/search.py @@ -0,0 +1,185 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import httpx + +from ..types import search_web_params +from .._types import Body, Omit, Query, Headers, NotGiven, omit, not_given +from .._utils import maybe_transform, 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.search_web_response import SearchWebResponse + +__all__ = ["SearchResource", "AsyncSearchResource"] + + +class SearchResource(SyncAPIResource): + @cached_property + def with_raw_response(self) -> SearchResourceWithRawResponse: + """ + 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/sdk-python#accessing-raw-response-data-eg-headers + """ + return SearchResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> SearchResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/browserbase/sdk-python#with_streaming_response + """ + return SearchResourceWithStreamingResponse(self) + + def web( + self, + *, + query: str, + num_results: 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, + ) -> SearchWebResponse: + """ + Perform a web search and return structured results. + + Args: + query: The search query string + + num_results: Number of results to return (1-25) + + 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( + "/v1/search", + body=maybe_transform( + { + "query": query, + "num_results": num_results, + }, + search_web_params.SearchWebParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=SearchWebResponse, + ) + + +class AsyncSearchResource(AsyncAPIResource): + @cached_property + def with_raw_response(self) -> AsyncSearchResourceWithRawResponse: + """ + 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/sdk-python#accessing-raw-response-data-eg-headers + """ + return AsyncSearchResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncSearchResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/browserbase/sdk-python#with_streaming_response + """ + return AsyncSearchResourceWithStreamingResponse(self) + + async def web( + self, + *, + query: str, + num_results: 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, + ) -> SearchWebResponse: + """ + Perform a web search and return structured results. + + Args: + query: The search query string + + num_results: Number of results to return (1-25) + + 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( + "/v1/search", + body=await async_maybe_transform( + { + "query": query, + "num_results": num_results, + }, + search_web_params.SearchWebParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=SearchWebResponse, + ) + + +class SearchResourceWithRawResponse: + def __init__(self, search: SearchResource) -> None: + self._search = search + + self.web = to_raw_response_wrapper( + search.web, + ) + + +class AsyncSearchResourceWithRawResponse: + def __init__(self, search: AsyncSearchResource) -> None: + self._search = search + + self.web = async_to_raw_response_wrapper( + search.web, + ) + + +class SearchResourceWithStreamingResponse: + def __init__(self, search: SearchResource) -> None: + self._search = search + + self.web = to_streamed_response_wrapper( + search.web, + ) + + +class AsyncSearchResourceWithStreamingResponse: + def __init__(self, search: AsyncSearchResource) -> None: + self._search = search + + self.web = async_to_streamed_response_wrapper( + search.web, + ) diff --git a/src/browserbase/types/__init__.py b/src/browserbase/types/__init__.py index 52659907..0a9a3b84 100644 --- a/src/browserbase/types/__init__.py +++ b/src/browserbase/types/__init__.py @@ -7,7 +7,9 @@ from .session import Session as Session from .extension import Extension as Extension from .project_usage import ProjectUsage as ProjectUsage +from .search_web_params import SearchWebParams as SearchWebParams from .session_live_urls import SessionLiveURLs as SessionLiveURLs +from .search_web_response import SearchWebResponse as SearchWebResponse from .session_list_params import SessionListParams as SessionListParams from .context_create_params import ContextCreateParams as ContextCreateParams from .project_list_response import ProjectListResponse as ProjectListResponse diff --git a/src/browserbase/types/search_web_params.py b/src/browserbase/types/search_web_params.py new file mode 100644 index 00000000..68926b56 --- /dev/null +++ b/src/browserbase/types/search_web_params.py @@ -0,0 +1,17 @@ +# 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 .._utils import PropertyInfo + +__all__ = ["SearchWebParams"] + + +class SearchWebParams(TypedDict, total=False): + query: Required[str] + """The search query string""" + + num_results: Annotated[int, PropertyInfo(alias="numResults")] + """Number of results to return (1-25)""" diff --git a/src/browserbase/types/search_web_response.py b/src/browserbase/types/search_web_response.py new file mode 100644 index 00000000..0243d22a --- /dev/null +++ b/src/browserbase/types/search_web_response.py @@ -0,0 +1,46 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List, Optional +from datetime import datetime + +from pydantic import Field as FieldInfo + +from .._models import BaseModel + +__all__ = ["SearchWebResponse", "Result"] + + +class Result(BaseModel): + id: str + """Unique identifier for the result""" + + title: str + """The title of the search result""" + + url: str + """The URL of the search result""" + + author: Optional[str] = None + """Author of the content if available""" + + favicon: Optional[str] = None + """Favicon URL""" + + image: Optional[str] = None + """Image URL if available""" + + published_date: Optional[datetime] = FieldInfo(alias="publishedDate", default=None) + """Publication date in ISO 8601 format""" + + +class SearchWebResponse(BaseModel): + """Response body for web search""" + + query: str + """The search query that was executed""" + + request_id: str = FieldInfo(alias="requestId") + """Unique identifier for the request""" + + results: List[Result] + """List of search results""" diff --git a/tests/api_resources/test_search.py b/tests/api_resources/test_search.py new file mode 100644 index 00000000..fa00b773 --- /dev/null +++ b/tests/api_resources/test_search.py @@ -0,0 +1,102 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import os +from typing import Any, cast + +import pytest + +from browserbase import Browserbase, AsyncBrowserbase +from tests.utils import assert_matches_type +from browserbase.types import SearchWebResponse + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + + +class TestSearch: + parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) + + @parametrize + def test_method_web(self, client: Browserbase) -> None: + search = client.search.web( + query="x", + ) + assert_matches_type(SearchWebResponse, search, path=["response"]) + + @parametrize + def test_method_web_with_all_params(self, client: Browserbase) -> None: + search = client.search.web( + query="x", + num_results=1, + ) + assert_matches_type(SearchWebResponse, search, path=["response"]) + + @parametrize + def test_raw_response_web(self, client: Browserbase) -> None: + response = client.search.with_raw_response.web( + query="x", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + search = response.parse() + assert_matches_type(SearchWebResponse, search, path=["response"]) + + @parametrize + def test_streaming_response_web(self, client: Browserbase) -> None: + with client.search.with_streaming_response.web( + query="x", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + search = response.parse() + assert_matches_type(SearchWebResponse, search, path=["response"]) + + assert cast(Any, response.is_closed) is True + + +class TestAsyncSearch: + parametrize = pytest.mark.parametrize( + "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] + ) + + @parametrize + async def test_method_web(self, async_client: AsyncBrowserbase) -> None: + search = await async_client.search.web( + query="x", + ) + assert_matches_type(SearchWebResponse, search, path=["response"]) + + @parametrize + async def test_method_web_with_all_params(self, async_client: AsyncBrowserbase) -> None: + search = await async_client.search.web( + query="x", + num_results=1, + ) + assert_matches_type(SearchWebResponse, search, path=["response"]) + + @parametrize + async def test_raw_response_web(self, async_client: AsyncBrowserbase) -> None: + response = await async_client.search.with_raw_response.web( + query="x", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + search = await response.parse() + assert_matches_type(SearchWebResponse, search, path=["response"]) + + @parametrize + async def test_streaming_response_web(self, async_client: AsyncBrowserbase) -> None: + async with async_client.search.with_streaming_response.web( + query="x", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + search = await response.parse() + assert_matches_type(SearchWebResponse, search, path=["response"]) + + assert cast(Any, response.is_closed) is True From 258af8190e3aadb117a1bb577001ce6519463b1f Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 16 Mar 2026 21:01:23 +0000 Subject: [PATCH 266/330] chore(internal): version bump --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/browserbase/_version.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 7deae338..cce9d1c6 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "1.6.0" + ".": "1.7.0" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index b72a7ccb..f053550d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "browserbase" -version = "1.6.0" +version = "1.7.0" description = "The official Python library for the Browserbase API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/browserbase/_version.py b/src/browserbase/_version.py index a7f6a7d4..1b8456be 100644 --- a/src/browserbase/_version.py +++ b/src/browserbase/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "browserbase" -__version__ = "1.6.0" # x-release-please-version +__version__ = "1.7.0" # x-release-please-version From 4f0a397e723357014c9b83183925f3cfe7ee30e9 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 17 Mar 2026 02:44:28 +0000 Subject: [PATCH 267/330] feat: Update search docs by removing description from typebox schemas --- .stats.yml | 4 ++-- src/browserbase/types/search_web_response.py | 2 -- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/.stats.yml b/.stats.yml index 28a1de1a..16938598 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 21 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fbrowserbase-9b1e2a2abf39dd780601935a9a9ee04cb939e2c3ba76627535f625b6aeaf5eb7.yml -openapi_spec_hash: 12fe5f4306c43fdfb394a33f79391a82 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fbrowserbase-4a2232ec1af8a1b569af75f732a5e82922f38c0f667a1e0fdeb186ca5be4b6bf.yml +openapi_spec_hash: a4cb7826eaf460b2b509c68f61949d9b config_hash: cf04ecfb8dad5fbd8b85be25d6e9ec55 diff --git a/src/browserbase/types/search_web_response.py b/src/browserbase/types/search_web_response.py index 0243d22a..4694ffa6 100644 --- a/src/browserbase/types/search_web_response.py +++ b/src/browserbase/types/search_web_response.py @@ -34,8 +34,6 @@ class Result(BaseModel): class SearchWebResponse(BaseModel): - """Response body for web search""" - query: str """The search query that was executed""" From d2ebb19850cba9a945144c1cb78a601652d1c33c Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 17 Mar 2026 02:54:47 +0000 Subject: [PATCH 268/330] fix(pydantic): do not pass `by_alias` unless set --- src/browserbase/_compat.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/browserbase/_compat.py b/src/browserbase/_compat.py index 786ff42a..e6690a4f 100644 --- a/src/browserbase/_compat.py +++ b/src/browserbase/_compat.py @@ -2,7 +2,7 @@ from typing import TYPE_CHECKING, Any, Union, Generic, TypeVar, Callable, cast, overload from datetime import date, datetime -from typing_extensions import Self, Literal +from typing_extensions import Self, Literal, TypedDict import pydantic from pydantic.fields import FieldInfo @@ -131,6 +131,10 @@ def model_json(model: pydantic.BaseModel, *, indent: int | None = None) -> str: return model.model_dump_json(indent=indent) +class _ModelDumpKwargs(TypedDict, total=False): + by_alias: bool + + def model_dump( model: pydantic.BaseModel, *, @@ -142,6 +146,9 @@ def model_dump( by_alias: bool | None = None, ) -> dict[str, Any]: if (not PYDANTIC_V1) or hasattr(model, "model_dump"): + kwargs: _ModelDumpKwargs = {} + if by_alias is not None: + kwargs["by_alias"] = by_alias return model.model_dump( mode=mode, exclude=exclude, @@ -149,7 +156,7 @@ def model_dump( exclude_defaults=exclude_defaults, # warnings are not supported in Pydantic v1 warnings=True if PYDANTIC_V1 else warnings, - by_alias=by_alias, + **kwargs, ) return cast( "dict[str, Any]", From f6cb5b2f1932dd274587b347520268bcf3b0b560 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 17 Mar 2026 03:00:09 +0000 Subject: [PATCH 269/330] fix(deps): bump minimum typing-extensions version --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index f053550d..5b3df704 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,7 +11,7 @@ authors = [ dependencies = [ "httpx>=0.23.0, <1", "pydantic>=1.9.0, <3", - "typing-extensions>=4.10, <5", + "typing-extensions>=4.14, <5", "anyio>=3.5.0, <5", "distro>=1.7.0, <2", "sniffio", From 50874987631ed3e1516e949afa2cba491f54fac4 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 17 Mar 2026 03:03:49 +0000 Subject: [PATCH 270/330] chore(internal): tweak CI branches --- .github/workflows/ci.yml | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0925562d..b9027983 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,12 +1,14 @@ name: CI on: push: - branches-ignore: - - 'generated' - - 'codegen/**' - - 'integrated/**' - - 'stl-preview-head/**' - - 'stl-preview-base/**' + branches: + - '**' + - '!integrated/**' + - '!stl-preview-head/**' + - '!stl-preview-base/**' + - '!generated' + - '!codegen/**' + - 'codegen/stl/**' pull_request: branches-ignore: - 'stl-preview-head/**' From 2fac8a998b6fc4ebcbb18dca14f25d190106ac2f Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 18 Mar 2026 18:33:51 +0000 Subject: [PATCH 271/330] codegen metadata --- .stats.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index 16938598..aa7031f8 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 21 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fbrowserbase-4a2232ec1af8a1b569af75f732a5e82922f38c0f667a1e0fdeb186ca5be4b6bf.yml -openapi_spec_hash: a4cb7826eaf460b2b509c68f61949d9b +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fbrowserbase-36f5c0b440d6e76d1ef6e69519dfda5c92309a331bcb24c4727179f887ea33d1.yml +openapi_spec_hash: d4303f8c771e98f8a9baf50771de7103 config_hash: cf04ecfb8dad5fbd8b85be25d6e9ec55 From 724445f0c96c8aa384cba593dd586c3688f739bc Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 20 Mar 2026 02:08:26 +0000 Subject: [PATCH 272/330] fix: sanitize endpoint path params --- src/browserbase/_utils/__init__.py | 1 + src/browserbase/_utils/_path.py | 127 ++++++++++++++++++ src/browserbase/resources/contexts.py | 14 +- src/browserbase/resources/extensions.py | 10 +- src/browserbase/resources/projects.py | 9 +- .../resources/sessions/downloads.py | 5 +- src/browserbase/resources/sessions/logs.py | 5 +- .../resources/sessions/recording.py | 5 +- .../resources/sessions/sessions.py | 14 +- src/browserbase/resources/sessions/uploads.py | 6 +- tests/test_utils/test_path.py | 89 ++++++++++++ 11 files changed, 253 insertions(+), 32 deletions(-) create mode 100644 src/browserbase/_utils/_path.py create mode 100644 tests/test_utils/test_path.py diff --git a/src/browserbase/_utils/__init__.py b/src/browserbase/_utils/__init__.py index dc64e29a..10cb66d2 100644 --- a/src/browserbase/_utils/__init__.py +++ b/src/browserbase/_utils/__init__.py @@ -1,3 +1,4 @@ +from ._path import path_template as path_template from ._sync import asyncify as asyncify from ._proxy import LazyProxy as LazyProxy from ._utils import ( diff --git a/src/browserbase/_utils/_path.py b/src/browserbase/_utils/_path.py new file mode 100644 index 00000000..4d6e1e4c --- /dev/null +++ b/src/browserbase/_utils/_path.py @@ -0,0 +1,127 @@ +from __future__ import annotations + +import re +from typing import ( + Any, + Mapping, + Callable, +) +from urllib.parse import quote + +# Matches '.' or '..' where each dot is either literal or percent-encoded (%2e / %2E). +_DOT_SEGMENT_RE = re.compile(r"^(?:\.|%2[eE]){1,2}$") + +_PLACEHOLDER_RE = re.compile(r"\{(\w+)\}") + + +def _quote_path_segment_part(value: str) -> str: + """Percent-encode `value` for use in a URI path segment. + + Considers characters not in `pchar` set from RFC 3986 §3.3 to be unsafe. + https://datatracker.ietf.org/doc/html/rfc3986#section-3.3 + """ + # quote() already treats unreserved characters (letters, digits, and -._~) + # as safe, so we only need to add sub-delims, ':', and '@'. + # Notably, unlike the default `safe` for quote(), / is unsafe and must be quoted. + return quote(value, safe="!$&'()*+,;=:@") + + +def _quote_query_part(value: str) -> str: + """Percent-encode `value` for use in a URI query string. + + Considers &, = and characters not in `query` set from RFC 3986 §3.4 to be unsafe. + https://datatracker.ietf.org/doc/html/rfc3986#section-3.4 + """ + return quote(value, safe="!$'()*+,;:@/?") + + +def _quote_fragment_part(value: str) -> str: + """Percent-encode `value` for use in a URI fragment. + + Considers characters not in `fragment` set from RFC 3986 §3.5 to be unsafe. + https://datatracker.ietf.org/doc/html/rfc3986#section-3.5 + """ + return quote(value, safe="!$&'()*+,;=:@/?") + + +def _interpolate( + template: str, + values: Mapping[str, Any], + quoter: Callable[[str], str], +) -> str: + """Replace {name} placeholders in `template`, quoting each value with `quoter`. + + Placeholder names are looked up in `values`. + + Raises: + KeyError: If a placeholder is not found in `values`. + """ + # re.split with a capturing group returns alternating + # [text, name, text, name, ..., text] elements. + parts = _PLACEHOLDER_RE.split(template) + + for i in range(1, len(parts), 2): + name = parts[i] + if name not in values: + raise KeyError(f"a value for placeholder {{{name}}} was not provided") + val = values[name] + if val is None: + parts[i] = "null" + elif isinstance(val, bool): + parts[i] = "true" if val else "false" + else: + parts[i] = quoter(str(values[name])) + + return "".join(parts) + + +def path_template(template: str, /, **kwargs: Any) -> str: + """Interpolate {name} placeholders in `template` from keyword arguments. + + Args: + template: The template string containing {name} placeholders. + **kwargs: Keyword arguments to interpolate into the template. + + Returns: + The template with placeholders interpolated and percent-encoded. + + Safe characters for percent-encoding are dependent on the URI component. + Placeholders in path and fragment portions are percent-encoded where the `segment` + and `fragment` sets from RFC 3986 respectively are considered safe. + Placeholders in the query portion are percent-encoded where the `query` set from + RFC 3986 §3.3 is considered safe except for = and & characters. + + Raises: + KeyError: If a placeholder is not found in `kwargs`. + ValueError: If resulting path contains /./ or /../ segments (including percent-encoded dot-segments). + """ + # Split the template into path, query, and fragment portions. + fragment_template: str | None = None + query_template: str | None = None + + rest = template + if "#" in rest: + rest, fragment_template = rest.split("#", 1) + if "?" in rest: + rest, query_template = rest.split("?", 1) + path_template = rest + + # Interpolate each portion with the appropriate quoting rules. + path_result = _interpolate(path_template, kwargs, _quote_path_segment_part) + + # Reject dot-segments (. and ..) in the final assembled path. The check + # runs after interpolation so that adjacent placeholders or a mix of static + # text and placeholders that together form a dot-segment are caught. + # Also reject percent-encoded dot-segments to protect against incorrectly + # implemented normalization in servers/proxies. + for segment in path_result.split("/"): + if _DOT_SEGMENT_RE.match(segment): + raise ValueError(f"Constructed path {path_result!r} contains dot-segment {segment!r} which is not allowed") + + result = path_result + if query_template is not None: + result += "?" + _interpolate(query_template, kwargs, _quote_query_part) + if fragment_template is not None: + result += "#" + _interpolate(fragment_template, kwargs, _quote_fragment_part) + + return result diff --git a/src/browserbase/resources/contexts.py b/src/browserbase/resources/contexts.py index 685daee4..f3f8f650 100644 --- a/src/browserbase/resources/contexts.py +++ b/src/browserbase/resources/contexts.py @@ -6,7 +6,7 @@ from ..types import context_create_params from .._types import Body, Omit, Query, Headers, NoneType, NotGiven, omit, not_given -from .._utils import maybe_transform, async_maybe_transform +from .._utils import path_template, maybe_transform, async_maybe_transform from .._compat import cached_property from .._resource import SyncAPIResource, AsyncAPIResource from .._response import ( @@ -106,7 +106,7 @@ def retrieve( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return self._get( - f"/v1/contexts/{id}", + path_template("/v1/contexts/{id}", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -139,7 +139,7 @@ def update( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return self._put( - f"/v1/contexts/{id}", + path_template("/v1/contexts/{id}", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -173,7 +173,7 @@ def delete( raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") extra_headers = {"Accept": "*/*", **(extra_headers or {})} return self._delete( - f"/v1/contexts/{id}", + path_template("/v1/contexts/{id}", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -264,7 +264,7 @@ async def retrieve( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return await self._get( - f"/v1/contexts/{id}", + path_template("/v1/contexts/{id}", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -297,7 +297,7 @@ async def update( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return await self._put( - f"/v1/contexts/{id}", + path_template("/v1/contexts/{id}", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -331,7 +331,7 @@ async def delete( raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") extra_headers = {"Accept": "*/*", **(extra_headers or {})} return await self._delete( - f"/v1/contexts/{id}", + path_template("/v1/contexts/{id}", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), diff --git a/src/browserbase/resources/extensions.py b/src/browserbase/resources/extensions.py index 534cd415..2d6fb1b0 100644 --- a/src/browserbase/resources/extensions.py +++ b/src/browserbase/resources/extensions.py @@ -8,7 +8,7 @@ from ..types import extension_create_params from .._types import Body, Query, Headers, NoneType, NotGiven, FileTypes, not_given -from .._utils import extract_files, maybe_transform, deepcopy_minimal, async_maybe_transform +from .._utils import extract_files, path_template, maybe_transform, deepcopy_minimal, async_maybe_transform from .._compat import cached_property from .._resource import SyncAPIResource, AsyncAPIResource from .._response import ( @@ -108,7 +108,7 @@ def retrieve( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return self._get( - f"/v1/extensions/{id}", + path_template("/v1/extensions/{id}", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -142,7 +142,7 @@ def delete( raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") extra_headers = {"Accept": "*/*", **(extra_headers or {})} return self._delete( - f"/v1/extensions/{id}", + path_template("/v1/extensions/{id}", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -235,7 +235,7 @@ async def retrieve( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return await self._get( - f"/v1/extensions/{id}", + path_template("/v1/extensions/{id}", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -269,7 +269,7 @@ async def delete( raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") extra_headers = {"Accept": "*/*", **(extra_headers or {})} return await self._delete( - f"/v1/extensions/{id}", + path_template("/v1/extensions/{id}", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), diff --git a/src/browserbase/resources/projects.py b/src/browserbase/resources/projects.py index 5ce225fd..791fe595 100644 --- a/src/browserbase/resources/projects.py +++ b/src/browserbase/resources/projects.py @@ -5,6 +5,7 @@ import httpx from .._types import Body, Query, Headers, NotGiven, not_given +from .._utils import path_template from .._compat import cached_property from .._resource import SyncAPIResource, AsyncAPIResource from .._response import ( @@ -67,7 +68,7 @@ def retrieve( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return self._get( - f"/v1/projects/{id}", + path_template("/v1/projects/{id}", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -119,7 +120,7 @@ def usage( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return self._get( - f"/v1/projects/{id}/usage", + path_template("/v1/projects/{id}/usage", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -173,7 +174,7 @@ async def retrieve( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return await self._get( - f"/v1/projects/{id}", + path_template("/v1/projects/{id}", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -225,7 +226,7 @@ async def usage( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return await self._get( - f"/v1/projects/{id}/usage", + path_template("/v1/projects/{id}/usage", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), diff --git a/src/browserbase/resources/sessions/downloads.py b/src/browserbase/resources/sessions/downloads.py index 6195c30b..2cff906c 100644 --- a/src/browserbase/resources/sessions/downloads.py +++ b/src/browserbase/resources/sessions/downloads.py @@ -5,6 +5,7 @@ import httpx from ..._types import Body, Query, Headers, NotGiven, not_given +from ..._utils import path_template from ..._compat import cached_property from ..._resource import SyncAPIResource, AsyncAPIResource from ..._response import ( @@ -69,7 +70,7 @@ def list( raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") extra_headers = {"Accept": "application/zip", **(extra_headers or {})} return self._get( - f"/v1/sessions/{id}/downloads", + path_template("/v1/sessions/{id}/downloads", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -124,7 +125,7 @@ async def list( raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") extra_headers = {"Accept": "application/zip", **(extra_headers or {})} return await self._get( - f"/v1/sessions/{id}/downloads", + path_template("/v1/sessions/{id}/downloads", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), diff --git a/src/browserbase/resources/sessions/logs.py b/src/browserbase/resources/sessions/logs.py index b1c90f52..55988c4c 100644 --- a/src/browserbase/resources/sessions/logs.py +++ b/src/browserbase/resources/sessions/logs.py @@ -5,6 +5,7 @@ import httpx from ..._types import Body, Query, Headers, NotGiven, not_given +from ..._utils import path_template from ..._compat import cached_property from ..._resource import SyncAPIResource, AsyncAPIResource from ..._response import ( @@ -65,7 +66,7 @@ def list( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return self._get( - f"/v1/sessions/{id}/logs", + path_template("/v1/sessions/{id}/logs", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -119,7 +120,7 @@ async def list( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return await self._get( - f"/v1/sessions/{id}/logs", + path_template("/v1/sessions/{id}/logs", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), diff --git a/src/browserbase/resources/sessions/recording.py b/src/browserbase/resources/sessions/recording.py index 789087a8..57e2d44e 100644 --- a/src/browserbase/resources/sessions/recording.py +++ b/src/browserbase/resources/sessions/recording.py @@ -5,6 +5,7 @@ import httpx from ..._types import Body, Query, Headers, NotGiven, not_given +from ..._utils import path_template from ..._compat import cached_property from ..._resource import SyncAPIResource, AsyncAPIResource from ..._response import ( @@ -65,7 +66,7 @@ def retrieve( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return self._get( - f"/v1/sessions/{id}/recording", + path_template("/v1/sessions/{id}/recording", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -119,7 +120,7 @@ async def retrieve( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return await self._get( - f"/v1/sessions/{id}/recording", + path_template("/v1/sessions/{id}/recording", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), diff --git a/src/browserbase/resources/sessions/sessions.py b/src/browserbase/resources/sessions/sessions.py index 35ff90ce..18bb0a73 100644 --- a/src/browserbase/resources/sessions/sessions.py +++ b/src/browserbase/resources/sessions/sessions.py @@ -25,7 +25,7 @@ AsyncUploadsResourceWithStreamingResponse, ) from ..._types import Body, Omit, Query, Headers, NotGiven, omit, not_given -from ..._utils import maybe_transform, async_maybe_transform +from ..._utils import path_template, maybe_transform, async_maybe_transform from ..._compat import cached_property from .downloads import ( DownloadsResource, @@ -195,7 +195,7 @@ def retrieve( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return self._get( - f"/v1/sessions/{id}", + path_template("/v1/sessions/{id}", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -237,7 +237,7 @@ def update( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return self._post( - f"/v1/sessions/{id}", + path_template("/v1/sessions/{id}", id=id), body=maybe_transform( { "status": status, @@ -324,7 +324,7 @@ def debug( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return self._get( - f"/v1/sessions/{id}/debug", + path_template("/v1/sessions/{id}/debug", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -467,7 +467,7 @@ async def retrieve( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return await self._get( - f"/v1/sessions/{id}", + path_template("/v1/sessions/{id}", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -509,7 +509,7 @@ async def update( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return await self._post( - f"/v1/sessions/{id}", + path_template("/v1/sessions/{id}", id=id), body=await async_maybe_transform( { "status": status, @@ -596,7 +596,7 @@ async def debug( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return await self._get( - f"/v1/sessions/{id}/debug", + path_template("/v1/sessions/{id}/debug", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), diff --git a/src/browserbase/resources/sessions/uploads.py b/src/browserbase/resources/sessions/uploads.py index aba72b64..7c776029 100644 --- a/src/browserbase/resources/sessions/uploads.py +++ b/src/browserbase/resources/sessions/uploads.py @@ -7,7 +7,7 @@ import httpx from ..._types import Body, Query, Headers, NotGiven, FileTypes, not_given -from ..._utils import extract_files, maybe_transform, deepcopy_minimal, async_maybe_transform +from ..._utils import extract_files, path_template, maybe_transform, deepcopy_minimal, async_maybe_transform from ..._compat import cached_property from ..._resource import SyncAPIResource, AsyncAPIResource from ..._response import ( @@ -76,7 +76,7 @@ def create( # multipart/form-data; boundary=---abc-- extra_headers = {"Content-Type": "multipart/form-data", **(extra_headers or {})} return self._post( - f"/v1/sessions/{id}/uploads", + path_template("/v1/sessions/{id}/uploads", id=id), body=maybe_transform(body, upload_create_params.UploadCreateParams), files=files, options=make_request_options( @@ -139,7 +139,7 @@ async def create( # multipart/form-data; boundary=---abc-- extra_headers = {"Content-Type": "multipart/form-data", **(extra_headers or {})} return await self._post( - f"/v1/sessions/{id}/uploads", + path_template("/v1/sessions/{id}/uploads", id=id), body=await async_maybe_transform(body, upload_create_params.UploadCreateParams), files=files, options=make_request_options( diff --git a/tests/test_utils/test_path.py b/tests/test_utils/test_path.py new file mode 100644 index 00000000..d1eb3ba3 --- /dev/null +++ b/tests/test_utils/test_path.py @@ -0,0 +1,89 @@ +from __future__ import annotations + +from typing import Any + +import pytest + +from browserbase._utils._path import path_template + + +@pytest.mark.parametrize( + "template, kwargs, expected", + [ + ("/v1/{id}", dict(id="abc"), "/v1/abc"), + ("/v1/{a}/{b}", dict(a="x", b="y"), "/v1/x/y"), + ("/v1/{a}{b}/path/{c}?val={d}#{e}", dict(a="x", b="y", c="z", d="u", e="v"), "/v1/xy/path/z?val=u#v"), + ("/{w}/{w}", dict(w="echo"), "/echo/echo"), + ("/v1/static", {}, "/v1/static"), + ("", {}, ""), + ("/v1/?q={n}&count=10", dict(n=42), "/v1/?q=42&count=10"), + ("/v1/{v}", dict(v=None), "/v1/null"), + ("/v1/{v}", dict(v=True), "/v1/true"), + ("/v1/{v}", dict(v=False), "/v1/false"), + ("/v1/{v}", dict(v=".hidden"), "/v1/.hidden"), # dot prefix ok + ("/v1/{v}", dict(v="file.txt"), "/v1/file.txt"), # dot in middle ok + ("/v1/{v}", dict(v="..."), "/v1/..."), # triple dot ok + ("/v1/{a}{b}", dict(a=".", b="txt"), "/v1/.txt"), # dot var combining with adjacent to be ok + ("/items?q={v}#{f}", dict(v=".", f=".."), "/items?q=.#.."), # dots in query/fragment are fine + ( + "/v1/{a}?query={b}", + dict(a="../../other/endpoint", b="a&bad=true"), + "/v1/..%2F..%2Fother%2Fendpoint?query=a%26bad%3Dtrue", + ), + ("/v1/{val}", dict(val="a/b/c"), "/v1/a%2Fb%2Fc"), + ("/v1/{val}", dict(val="a/b/c?query=value"), "/v1/a%2Fb%2Fc%3Fquery=value"), + ("/v1/{val}", dict(val="a/b/c?query=value&bad=true"), "/v1/a%2Fb%2Fc%3Fquery=value&bad=true"), + ("/v1/{val}", dict(val="%20"), "/v1/%2520"), # escapes escape sequences in input + # Query: slash and ? are safe, # is not + ("/items?q={v}", dict(v="a/b"), "/items?q=a/b"), + ("/items?q={v}", dict(v="a?b"), "/items?q=a?b"), + ("/items?q={v}", dict(v="a#b"), "/items?q=a%23b"), + ("/items?q={v}", dict(v="a b"), "/items?q=a%20b"), + # Fragment: slash and ? are safe + ("/docs#{v}", dict(v="a/b"), "/docs#a/b"), + ("/docs#{v}", dict(v="a?b"), "/docs#a?b"), + # Path: slash, ? and # are all encoded + ("/v1/{v}", dict(v="a/b"), "/v1/a%2Fb"), + ("/v1/{v}", dict(v="a?b"), "/v1/a%3Fb"), + ("/v1/{v}", dict(v="a#b"), "/v1/a%23b"), + # same var encoded differently by component + ( + "/v1/{v}?q={v}#{v}", + dict(v="a/b?c#d"), + "/v1/a%2Fb%3Fc%23d?q=a/b?c%23d#a/b?c%23d", + ), + ("/v1/{val}", dict(val="x?admin=true"), "/v1/x%3Fadmin=true"), # query injection + ("/v1/{val}", dict(val="x#admin"), "/v1/x%23admin"), # fragment injection + ], +) +def test_interpolation(template: str, kwargs: dict[str, Any], expected: str) -> None: + assert path_template(template, **kwargs) == expected + + +def test_missing_kwarg_raises_key_error() -> None: + with pytest.raises(KeyError, match="org_id"): + path_template("/v1/{org_id}") + + +@pytest.mark.parametrize( + "template, kwargs", + [ + ("{a}/path", dict(a=".")), + ("{a}/path", dict(a="..")), + ("/v1/{a}", dict(a=".")), + ("/v1/{a}", dict(a="..")), + ("/v1/{a}/path", dict(a=".")), + ("/v1/{a}/path", dict(a="..")), + ("/v1/{a}{b}", dict(a=".", b=".")), # adjacent vars → ".." + ("/v1/{a}.", dict(a=".")), # var + static → ".." + ("/v1/{a}{b}", dict(a="", b=".")), # empty + dot → "." + ("/v1/%2e/{x}", dict(x="ok")), # encoded dot in static text + ("/v1/%2e./{x}", dict(x="ok")), # mixed encoded ".." in static + ("/v1/.%2E/{x}", dict(x="ok")), # mixed encoded ".." in static + ("/v1/{v}?q=1", dict(v="..")), + ("/v1/{v}#frag", dict(v="..")), + ], +) +def test_dot_segment_rejected(template: str, kwargs: dict[str, Any]) -> None: + with pytest.raises(ValueError, match="dot-segment"): + path_template(template, **kwargs) From 2d08204ab134b956716fe663f9bee8bf21ac9cdc Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 20 Mar 2026 02:10:01 +0000 Subject: [PATCH 273/330] refactor(tests): switch from prism to steady --- CONTRIBUTING.md | 2 +- scripts/mock | 26 +++++++++++++------------- scripts/test | 16 ++++++++-------- 3 files changed, 22 insertions(+), 22 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4f99ed67..09a6a8a9 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -85,7 +85,7 @@ $ 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. +Most tests require you to [set up a mock server](https://github.com/dgellow/steady) against the OpenAPI spec to run the tests. ```sh $ ./scripts/mock diff --git a/scripts/mock b/scripts/mock index bcf3b392..38201de8 100755 --- a/scripts/mock +++ b/scripts/mock @@ -19,34 +19,34 @@ fi echo "==> Starting mock server with URL ${URL}" -# Run prism mock on the given spec +# Run steady mock on the given spec if [ "$1" == "--daemon" ]; then # Pre-install the package so the download doesn't eat into the startup timeout - npm exec --package=@stainless-api/prism-cli@5.15.0 -- prism --version + npm exec --package=@stdy/cli@0.19.3 -- steady --version - npm exec --package=@stainless-api/prism-cli@5.15.0 -- prism mock "$URL" &> .prism.log & + npm exec --package=@stdy/cli@0.19.3 -- steady --host 127.0.0.1 -p 4010 --validator-query-array-format=comma --validator-query-object-format=brackets "$URL" &> .stdy.log & - # Wait for server to come online (max 30s) + # Wait for server to come online via health endpoint (max 30s) echo -n "Waiting for server" attempts=0 - while ! grep -q "✖ fatal\|Prism is listening" ".prism.log" ; do + while ! curl --silent --fail "http://127.0.0.1:4010/_x-steady/health" >/dev/null 2>&1; do + if ! kill -0 $! 2>/dev/null; then + echo + cat .stdy.log + exit 1 + fi attempts=$((attempts + 1)) if [ "$attempts" -ge 300 ]; then echo - echo "Timed out waiting for Prism server to start" - cat .prism.log + echo "Timed out waiting for Steady server to start" + cat .stdy.log exit 1 fi 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" + npm exec --package=@stdy/cli@0.19.3 -- steady --host 127.0.0.1 -p 4010 --validator-query-array-format=comma --validator-query-object-format=brackets "$URL" fi diff --git a/scripts/test b/scripts/test index dbeda2d2..2dfdc409 100755 --- a/scripts/test +++ b/scripts/test @@ -9,8 +9,8 @@ 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 +function steady_is_running() { + curl --silent "http://127.0.0.1:4010/_x-steady/health" >/dev/null 2>&1 } kill_server_on_port() { @@ -25,7 +25,7 @@ function is_overriding_api_base_url() { [ -n "$TEST_API_BASE_URL" ] } -if ! is_overriding_api_base_url && ! prism_is_running ; then +if ! is_overriding_api_base_url && ! steady_is_running ; then # When we exit this script, make sure to kill the background mock server process trap 'kill_server_on_port 4010' EXIT @@ -36,19 +36,19 @@ 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" +elif ! steady_is_running ; then + echo -e "${RED}ERROR:${NC} The test suite will not run without a mock Steady 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 -e "spec to the steady command:" echo - echo -e " \$ ${YELLOW}npm exec --package=@stainless-api/prism-cli@5.15.0 -- prism mock path/to/your.openapi.yml${NC}" + echo -e " \$ ${YELLOW}npm exec --package=@stdy/cli@0.19.3 -- steady path/to/your.openapi.yml --host 127.0.0.1 -p 4010 --validator-query-array-format=comma --validator-query-object-format=brackets${NC}" echo exit 1 else - echo -e "${GREEN}✔ Mock prism server is running with your OpenAPI spec${NC}" + echo -e "${GREEN}✔ Mock steady server is running with your OpenAPI spec${NC}" echo fi From 38ab8a51c031fbdc7eb4a4fc459ee058f25d50ce Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 21 Mar 2026 02:13:34 +0000 Subject: [PATCH 274/330] chore(tests): bump steady to v0.19.4 --- scripts/mock | 6 +++--- scripts/test | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/scripts/mock b/scripts/mock index 38201de8..e1c19e88 100755 --- a/scripts/mock +++ b/scripts/mock @@ -22,9 +22,9 @@ echo "==> Starting mock server with URL ${URL}" # Run steady mock on the given spec if [ "$1" == "--daemon" ]; then # Pre-install the package so the download doesn't eat into the startup timeout - npm exec --package=@stdy/cli@0.19.3 -- steady --version + npm exec --package=@stdy/cli@0.19.4 -- steady --version - npm exec --package=@stdy/cli@0.19.3 -- steady --host 127.0.0.1 -p 4010 --validator-query-array-format=comma --validator-query-object-format=brackets "$URL" &> .stdy.log & + npm exec --package=@stdy/cli@0.19.4 -- steady --host 127.0.0.1 -p 4010 --validator-form-array-format=comma --validator-query-array-format=comma --validator-form-object-format=brackets --validator-query-object-format=brackets "$URL" &> .stdy.log & # Wait for server to come online via health endpoint (max 30s) echo -n "Waiting for server" @@ -48,5 +48,5 @@ if [ "$1" == "--daemon" ]; then echo else - npm exec --package=@stdy/cli@0.19.3 -- steady --host 127.0.0.1 -p 4010 --validator-query-array-format=comma --validator-query-object-format=brackets "$URL" + npm exec --package=@stdy/cli@0.19.4 -- steady --host 127.0.0.1 -p 4010 --validator-form-array-format=comma --validator-query-array-format=comma --validator-form-object-format=brackets --validator-query-object-format=brackets "$URL" fi diff --git a/scripts/test b/scripts/test index 2dfdc409..36fab0ae 100755 --- a/scripts/test +++ b/scripts/test @@ -43,7 +43,7 @@ elif ! steady_is_running ; then echo -e "To run the server, pass in the path or url of your OpenAPI" echo -e "spec to the steady command:" echo - echo -e " \$ ${YELLOW}npm exec --package=@stdy/cli@0.19.3 -- steady path/to/your.openapi.yml --host 127.0.0.1 -p 4010 --validator-query-array-format=comma --validator-query-object-format=brackets${NC}" + echo -e " \$ ${YELLOW}npm exec --package=@stdy/cli@0.19.4 -- steady path/to/your.openapi.yml --host 127.0.0.1 -p 4010 --validator-form-array-format=comma --validator-query-array-format=comma --validator-form-object-format=brackets --validator-query-object-format=brackets${NC}" echo exit 1 From 7d7c82ce3709ef2bd4b26df8fe5cbdb4aa683360 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 21 Mar 2026 02:20:32 +0000 Subject: [PATCH 275/330] chore(tests): bump steady to v0.19.5 --- scripts/mock | 6 +++--- scripts/test | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/scripts/mock b/scripts/mock index e1c19e88..ab814d38 100755 --- a/scripts/mock +++ b/scripts/mock @@ -22,9 +22,9 @@ echo "==> Starting mock server with URL ${URL}" # Run steady mock on the given spec if [ "$1" == "--daemon" ]; then # Pre-install the package so the download doesn't eat into the startup timeout - npm exec --package=@stdy/cli@0.19.4 -- steady --version + npm exec --package=@stdy/cli@0.19.5 -- steady --version - npm exec --package=@stdy/cli@0.19.4 -- steady --host 127.0.0.1 -p 4010 --validator-form-array-format=comma --validator-query-array-format=comma --validator-form-object-format=brackets --validator-query-object-format=brackets "$URL" &> .stdy.log & + npm exec --package=@stdy/cli@0.19.5 -- steady --host 127.0.0.1 -p 4010 --validator-form-array-format=comma --validator-query-array-format=comma --validator-form-object-format=brackets --validator-query-object-format=brackets "$URL" &> .stdy.log & # Wait for server to come online via health endpoint (max 30s) echo -n "Waiting for server" @@ -48,5 +48,5 @@ if [ "$1" == "--daemon" ]; then echo else - npm exec --package=@stdy/cli@0.19.4 -- steady --host 127.0.0.1 -p 4010 --validator-form-array-format=comma --validator-query-array-format=comma --validator-form-object-format=brackets --validator-query-object-format=brackets "$URL" + npm exec --package=@stdy/cli@0.19.5 -- steady --host 127.0.0.1 -p 4010 --validator-form-array-format=comma --validator-query-array-format=comma --validator-form-object-format=brackets --validator-query-object-format=brackets "$URL" fi diff --git a/scripts/test b/scripts/test index 36fab0ae..d1c8e1a9 100755 --- a/scripts/test +++ b/scripts/test @@ -43,7 +43,7 @@ elif ! steady_is_running ; then echo -e "To run the server, pass in the path or url of your OpenAPI" echo -e "spec to the steady command:" echo - echo -e " \$ ${YELLOW}npm exec --package=@stdy/cli@0.19.4 -- steady path/to/your.openapi.yml --host 127.0.0.1 -p 4010 --validator-form-array-format=comma --validator-query-array-format=comma --validator-form-object-format=brackets --validator-query-object-format=brackets${NC}" + echo -e " \$ ${YELLOW}npm exec --package=@stdy/cli@0.19.5 -- steady path/to/your.openapi.yml --host 127.0.0.1 -p 4010 --validator-form-array-format=comma --validator-query-array-format=comma --validator-form-object-format=brackets --validator-query-object-format=brackets${NC}" echo exit 1 From 2086f02945877bb400ab244269743d4c0afef315 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 24 Mar 2026 02:11:09 +0000 Subject: [PATCH 276/330] chore(internal): update gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 95ceb189..3824f4c4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ .prism.log +.stdy.log _dev __pycache__ From b6361b2ec319cfa5cf0822a275770b04cad0b628 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 24 Mar 2026 02:18:55 +0000 Subject: [PATCH 277/330] chore(tests): bump steady to v0.19.6 --- scripts/mock | 6 +++--- scripts/test | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/scripts/mock b/scripts/mock index ab814d38..b319bdfb 100755 --- a/scripts/mock +++ b/scripts/mock @@ -22,9 +22,9 @@ echo "==> Starting mock server with URL ${URL}" # Run steady mock on the given spec if [ "$1" == "--daemon" ]; then # Pre-install the package so the download doesn't eat into the startup timeout - npm exec --package=@stdy/cli@0.19.5 -- steady --version + npm exec --package=@stdy/cli@0.19.6 -- steady --version - npm exec --package=@stdy/cli@0.19.5 -- steady --host 127.0.0.1 -p 4010 --validator-form-array-format=comma --validator-query-array-format=comma --validator-form-object-format=brackets --validator-query-object-format=brackets "$URL" &> .stdy.log & + npm exec --package=@stdy/cli@0.19.6 -- steady --host 127.0.0.1 -p 4010 --validator-form-array-format=comma --validator-query-array-format=comma --validator-form-object-format=brackets --validator-query-object-format=brackets "$URL" &> .stdy.log & # Wait for server to come online via health endpoint (max 30s) echo -n "Waiting for server" @@ -48,5 +48,5 @@ if [ "$1" == "--daemon" ]; then echo else - npm exec --package=@stdy/cli@0.19.5 -- steady --host 127.0.0.1 -p 4010 --validator-form-array-format=comma --validator-query-array-format=comma --validator-form-object-format=brackets --validator-query-object-format=brackets "$URL" + npm exec --package=@stdy/cli@0.19.6 -- steady --host 127.0.0.1 -p 4010 --validator-form-array-format=comma --validator-query-array-format=comma --validator-form-object-format=brackets --validator-query-object-format=brackets "$URL" fi diff --git a/scripts/test b/scripts/test index d1c8e1a9..ab01948b 100755 --- a/scripts/test +++ b/scripts/test @@ -43,7 +43,7 @@ elif ! steady_is_running ; then echo -e "To run the server, pass in the path or url of your OpenAPI" echo -e "spec to the steady command:" echo - echo -e " \$ ${YELLOW}npm exec --package=@stdy/cli@0.19.5 -- steady path/to/your.openapi.yml --host 127.0.0.1 -p 4010 --validator-form-array-format=comma --validator-query-array-format=comma --validator-form-object-format=brackets --validator-query-object-format=brackets${NC}" + echo -e " \$ ${YELLOW}npm exec --package=@stdy/cli@0.19.6 -- steady path/to/your.openapi.yml --host 127.0.0.1 -p 4010 --validator-form-array-format=comma --validator-query-array-format=comma --validator-form-object-format=brackets --validator-query-object-format=brackets${NC}" echo exit 1 From 35f3f2046ff16cd2f4294968f0d5920807768742 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 24 Mar 2026 18:16:03 +0000 Subject: [PATCH 278/330] codegen metadata --- .stats.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index aa7031f8..8a28449c 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 21 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fbrowserbase-36f5c0b440d6e76d1ef6e69519dfda5c92309a331bcb24c4727179f887ea33d1.yml -openapi_spec_hash: d4303f8c771e98f8a9baf50771de7103 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fbrowserbase-ed18673546a3d1f7a54bf668fdeac794e741cef27e2ac63eee5ad639c9051edd.yml +openapi_spec_hash: a8b00618c51e0912669575644cea9efe config_hash: cf04ecfb8dad5fbd8b85be25d6e9ec55 From 915a1fedd4ce911775a5e94d82c8781fcbb05654 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 24 Mar 2026 22:42:17 +0000 Subject: [PATCH 279/330] codegen metadata --- .stats.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index 8a28449c..f736cfe5 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 21 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fbrowserbase-ed18673546a3d1f7a54bf668fdeac794e741cef27e2ac63eee5ad639c9051edd.yml -openapi_spec_hash: a8b00618c51e0912669575644cea9efe +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fbrowserbase-46fd56f25cb6fac7dd75497fed0e748c661b6ff35790c8d514e2abaa76881c7b.yml +openapi_spec_hash: 603b95fb22027269fb69ce3a093387f5 config_hash: cf04ecfb8dad5fbd8b85be25d6e9ec55 From c134bfc56d94df830c853e4c0f2e4fce084bc14e Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 24 Mar 2026 22:51:59 +0000 Subject: [PATCH 280/330] codegen metadata --- .stats.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index f736cfe5..2291e8e8 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 21 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fbrowserbase-46fd56f25cb6fac7dd75497fed0e748c661b6ff35790c8d514e2abaa76881c7b.yml -openapi_spec_hash: 603b95fb22027269fb69ce3a093387f5 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fbrowserbase-5a71e6b8b6f67aa8711ced43e3a5c4fa71e8462f49b0b0fdb56874cf6c153211.yml +openapi_spec_hash: 87c6c05f74bb1fe91075671d78c1885b config_hash: cf04ecfb8dad5fbd8b85be25d6e9ec55 From 0456d196c29e19d16415e0e5c4797c31ec0465f1 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 24 Mar 2026 23:00:51 +0000 Subject: [PATCH 281/330] codegen metadata --- .stats.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index 2291e8e8..4eabc782 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 21 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fbrowserbase-5a71e6b8b6f67aa8711ced43e3a5c4fa71e8462f49b0b0fdb56874cf6c153211.yml -openapi_spec_hash: 87c6c05f74bb1fe91075671d78c1885b +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fbrowserbase-63cfed1b24172acd4ed1a7731cbfff7d49b2641bce740c108dc5c7af4603cc0c.yml +openapi_spec_hash: 1be289eb666e8eab768fdbbb1b43df23 config_hash: cf04ecfb8dad5fbd8b85be25d6e9ec55 From df8e9af818bdbfd55abb9624a6bd6de065f732b6 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 24 Mar 2026 23:09:50 +0000 Subject: [PATCH 282/330] codegen metadata --- .stats.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index 4eabc782..e113c1f9 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 21 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fbrowserbase-63cfed1b24172acd4ed1a7731cbfff7d49b2641bce740c108dc5c7af4603cc0c.yml -openapi_spec_hash: 1be289eb666e8eab768fdbbb1b43df23 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fbrowserbase-deaa308eab2bc55e832c1d41f7ebeca3b0c585365d3909245c9023bd92f0c138.yml +openapi_spec_hash: ae263ee37e1172f8a27d45eac2637725 config_hash: cf04ecfb8dad5fbd8b85be25d6e9ec55 From 59883104bd84f5588cc927395df5719f083f7bfb Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 25 Mar 2026 02:06:20 +0000 Subject: [PATCH 283/330] chore(ci): skip lint on metadata-only changes Note that we still want to run tests, as these depend on the metadata. --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b9027983..9c4beaa4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,7 +19,7 @@ jobs: timeout-minutes: 10 name: lint runs-on: ${{ github.repository == 'stainless-sdks/browserbase-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} - if: github.event_name == 'push' || github.event.pull_request.head.repo.fork + if: (github.event_name == 'push' || github.event.pull_request.head.repo.fork) && (github.event_name != 'push' || github.event.head_commit.message != 'codegen metadata') steps: - uses: actions/checkout@v6 @@ -38,7 +38,7 @@ jobs: run: ./scripts/lint build: - if: github.event_name == 'push' || github.event.pull_request.head.repo.fork + if: (github.event_name == 'push' || github.event.pull_request.head.repo.fork) && (github.event_name != 'push' || github.event.head_commit.message != 'codegen metadata') timeout-minutes: 10 name: build permissions: From c7eb8783b8338f60445ae9991d65c7669d3a662e Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 25 Mar 2026 02:07:09 +0000 Subject: [PATCH 284/330] chore(tests): bump steady to v0.19.7 --- scripts/mock | 6 +++--- scripts/test | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/scripts/mock b/scripts/mock index b319bdfb..09eb49f6 100755 --- a/scripts/mock +++ b/scripts/mock @@ -22,9 +22,9 @@ echo "==> Starting mock server with URL ${URL}" # Run steady mock on the given spec if [ "$1" == "--daemon" ]; then # Pre-install the package so the download doesn't eat into the startup timeout - npm exec --package=@stdy/cli@0.19.6 -- steady --version + npm exec --package=@stdy/cli@0.19.7 -- steady --version - npm exec --package=@stdy/cli@0.19.6 -- steady --host 127.0.0.1 -p 4010 --validator-form-array-format=comma --validator-query-array-format=comma --validator-form-object-format=brackets --validator-query-object-format=brackets "$URL" &> .stdy.log & + npm exec --package=@stdy/cli@0.19.7 -- steady --host 127.0.0.1 -p 4010 --validator-form-array-format=comma --validator-query-array-format=comma --validator-form-object-format=brackets --validator-query-object-format=brackets "$URL" &> .stdy.log & # Wait for server to come online via health endpoint (max 30s) echo -n "Waiting for server" @@ -48,5 +48,5 @@ if [ "$1" == "--daemon" ]; then echo else - npm exec --package=@stdy/cli@0.19.6 -- steady --host 127.0.0.1 -p 4010 --validator-form-array-format=comma --validator-query-array-format=comma --validator-form-object-format=brackets --validator-query-object-format=brackets "$URL" + npm exec --package=@stdy/cli@0.19.7 -- steady --host 127.0.0.1 -p 4010 --validator-form-array-format=comma --validator-query-array-format=comma --validator-form-object-format=brackets --validator-query-object-format=brackets "$URL" fi diff --git a/scripts/test b/scripts/test index ab01948b..e46b9b58 100755 --- a/scripts/test +++ b/scripts/test @@ -43,7 +43,7 @@ elif ! steady_is_running ; then echo -e "To run the server, pass in the path or url of your OpenAPI" echo -e "spec to the steady command:" echo - echo -e " \$ ${YELLOW}npm exec --package=@stdy/cli@0.19.6 -- steady path/to/your.openapi.yml --host 127.0.0.1 -p 4010 --validator-form-array-format=comma --validator-query-array-format=comma --validator-form-object-format=brackets --validator-query-object-format=brackets${NC}" + echo -e " \$ ${YELLOW}npm exec --package=@stdy/cli@0.19.7 -- steady path/to/your.openapi.yml --host 127.0.0.1 -p 4010 --validator-form-array-format=comma --validator-query-array-format=comma --validator-form-object-format=brackets --validator-query-object-format=brackets${NC}" echo exit 1 From 16809eed63665d13f8d4583eefcc374c76333ba8 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 27 Mar 2026 02:26:25 +0000 Subject: [PATCH 285/330] feat(internal): implement indices array format for query and form serialization --- scripts/mock | 4 ++-- scripts/test | 2 +- src/browserbase/_qs.py | 5 ++++- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/scripts/mock b/scripts/mock index 09eb49f6..290e21b9 100755 --- a/scripts/mock +++ b/scripts/mock @@ -24,7 +24,7 @@ if [ "$1" == "--daemon" ]; then # Pre-install the package so the download doesn't eat into the startup timeout npm exec --package=@stdy/cli@0.19.7 -- steady --version - npm exec --package=@stdy/cli@0.19.7 -- steady --host 127.0.0.1 -p 4010 --validator-form-array-format=comma --validator-query-array-format=comma --validator-form-object-format=brackets --validator-query-object-format=brackets "$URL" &> .stdy.log & + npm exec --package=@stdy/cli@0.19.7 -- steady --host 127.0.0.1 -p 4010 --validator-query-array-format=comma --validator-form-array-format=comma --validator-query-object-format=brackets --validator-form-object-format=brackets "$URL" &> .stdy.log & # Wait for server to come online via health endpoint (max 30s) echo -n "Waiting for server" @@ -48,5 +48,5 @@ if [ "$1" == "--daemon" ]; then echo else - npm exec --package=@stdy/cli@0.19.7 -- steady --host 127.0.0.1 -p 4010 --validator-form-array-format=comma --validator-query-array-format=comma --validator-form-object-format=brackets --validator-query-object-format=brackets "$URL" + npm exec --package=@stdy/cli@0.19.7 -- steady --host 127.0.0.1 -p 4010 --validator-query-array-format=comma --validator-form-array-format=comma --validator-query-object-format=brackets --validator-form-object-format=brackets "$URL" fi diff --git a/scripts/test b/scripts/test index e46b9b58..661f9bf4 100755 --- a/scripts/test +++ b/scripts/test @@ -43,7 +43,7 @@ elif ! steady_is_running ; then echo -e "To run the server, pass in the path or url of your OpenAPI" echo -e "spec to the steady command:" echo - echo -e " \$ ${YELLOW}npm exec --package=@stdy/cli@0.19.7 -- steady path/to/your.openapi.yml --host 127.0.0.1 -p 4010 --validator-form-array-format=comma --validator-query-array-format=comma --validator-form-object-format=brackets --validator-query-object-format=brackets${NC}" + echo -e " \$ ${YELLOW}npm exec --package=@stdy/cli@0.19.7 -- steady path/to/your.openapi.yml --host 127.0.0.1 -p 4010 --validator-query-array-format=comma --validator-form-array-format=comma --validator-query-object-format=brackets --validator-form-object-format=brackets${NC}" echo exit 1 diff --git a/src/browserbase/_qs.py b/src/browserbase/_qs.py index ada6fd3f..de8c99bc 100644 --- a/src/browserbase/_qs.py +++ b/src/browserbase/_qs.py @@ -101,7 +101,10 @@ def _stringify_item( items.extend(self._stringify_item(key, item, opts)) return items elif array_format == "indices": - raise NotImplementedError("The array indices format is not supported yet") + items = [] + for i, item in enumerate(value): + items.extend(self._stringify_item(f"{key}[{i}]", item, opts)) + return items elif array_format == "brackets": items = [] key = key + "[]" From 8278b4aee2de50c1d0c93ca2da004e743ab9a177 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sun, 29 Mar 2026 21:49:02 +0000 Subject: [PATCH 286/330] codegen metadata --- .stats.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index e113c1f9..96c76817 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 21 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fbrowserbase-deaa308eab2bc55e832c1d41f7ebeca3b0c585365d3909245c9023bd92f0c138.yml -openapi_spec_hash: ae263ee37e1172f8a27d45eac2637725 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fbrowserbase-27e5ad306f9f631a025e0e4047ac1b2d1e7674fa533f99800aa83eb68a19c98b.yml +openapi_spec_hash: eb6e27e1aa8c906d8c8b007830ec7e02 config_hash: cf04ecfb8dad5fbd8b85be25d6e9ec55 From 4e4bb66eaab1f1478b8ba4f59f72ef1dabc35b0d Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 1 Apr 2026 02:22:35 +0000 Subject: [PATCH 287/330] chore(tests): bump steady to v0.20.1 --- scripts/mock | 6 +++--- scripts/test | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/scripts/mock b/scripts/mock index 290e21b9..15c29941 100755 --- a/scripts/mock +++ b/scripts/mock @@ -22,9 +22,9 @@ echo "==> Starting mock server with URL ${URL}" # Run steady mock on the given spec if [ "$1" == "--daemon" ]; then # Pre-install the package so the download doesn't eat into the startup timeout - npm exec --package=@stdy/cli@0.19.7 -- steady --version + npm exec --package=@stdy/cli@0.20.1 -- steady --version - npm exec --package=@stdy/cli@0.19.7 -- steady --host 127.0.0.1 -p 4010 --validator-query-array-format=comma --validator-form-array-format=comma --validator-query-object-format=brackets --validator-form-object-format=brackets "$URL" &> .stdy.log & + npm exec --package=@stdy/cli@0.20.1 -- steady --host 127.0.0.1 -p 4010 --validator-query-array-format=comma --validator-form-array-format=comma --validator-query-object-format=brackets --validator-form-object-format=brackets "$URL" &> .stdy.log & # Wait for server to come online via health endpoint (max 30s) echo -n "Waiting for server" @@ -48,5 +48,5 @@ if [ "$1" == "--daemon" ]; then echo else - npm exec --package=@stdy/cli@0.19.7 -- steady --host 127.0.0.1 -p 4010 --validator-query-array-format=comma --validator-form-array-format=comma --validator-query-object-format=brackets --validator-form-object-format=brackets "$URL" + npm exec --package=@stdy/cli@0.20.1 -- steady --host 127.0.0.1 -p 4010 --validator-query-array-format=comma --validator-form-array-format=comma --validator-query-object-format=brackets --validator-form-object-format=brackets "$URL" fi diff --git a/scripts/test b/scripts/test index 661f9bf4..c8e2e9d5 100755 --- a/scripts/test +++ b/scripts/test @@ -43,7 +43,7 @@ elif ! steady_is_running ; then echo -e "To run the server, pass in the path or url of your OpenAPI" echo -e "spec to the steady command:" echo - echo -e " \$ ${YELLOW}npm exec --package=@stdy/cli@0.19.7 -- steady path/to/your.openapi.yml --host 127.0.0.1 -p 4010 --validator-query-array-format=comma --validator-form-array-format=comma --validator-query-object-format=brackets --validator-form-object-format=brackets${NC}" + echo -e " \$ ${YELLOW}npm exec --package=@stdy/cli@0.20.1 -- steady path/to/your.openapi.yml --host 127.0.0.1 -p 4010 --validator-query-array-format=comma --validator-form-array-format=comma --validator-query-object-format=brackets --validator-form-object-format=brackets${NC}" echo exit 1 From 1e74f2413461fd526382cb8052fc6603e8742cc6 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 1 Apr 2026 02:26:18 +0000 Subject: [PATCH 288/330] chore(tests): bump steady to v0.20.2 --- scripts/mock | 6 +++--- scripts/test | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/scripts/mock b/scripts/mock index 15c29941..5cd7c157 100755 --- a/scripts/mock +++ b/scripts/mock @@ -22,9 +22,9 @@ echo "==> Starting mock server with URL ${URL}" # Run steady mock on the given spec if [ "$1" == "--daemon" ]; then # Pre-install the package so the download doesn't eat into the startup timeout - npm exec --package=@stdy/cli@0.20.1 -- steady --version + npm exec --package=@stdy/cli@0.20.2 -- steady --version - npm exec --package=@stdy/cli@0.20.1 -- steady --host 127.0.0.1 -p 4010 --validator-query-array-format=comma --validator-form-array-format=comma --validator-query-object-format=brackets --validator-form-object-format=brackets "$URL" &> .stdy.log & + npm exec --package=@stdy/cli@0.20.2 -- steady --host 127.0.0.1 -p 4010 --validator-query-array-format=comma --validator-form-array-format=comma --validator-query-object-format=brackets --validator-form-object-format=brackets "$URL" &> .stdy.log & # Wait for server to come online via health endpoint (max 30s) echo -n "Waiting for server" @@ -48,5 +48,5 @@ if [ "$1" == "--daemon" ]; then echo else - npm exec --package=@stdy/cli@0.20.1 -- steady --host 127.0.0.1 -p 4010 --validator-query-array-format=comma --validator-form-array-format=comma --validator-query-object-format=brackets --validator-form-object-format=brackets "$URL" + npm exec --package=@stdy/cli@0.20.2 -- steady --host 127.0.0.1 -p 4010 --validator-query-array-format=comma --validator-form-array-format=comma --validator-query-object-format=brackets --validator-form-object-format=brackets "$URL" fi diff --git a/scripts/test b/scripts/test index c8e2e9d5..b8143aa3 100755 --- a/scripts/test +++ b/scripts/test @@ -43,7 +43,7 @@ elif ! steady_is_running ; then echo -e "To run the server, pass in the path or url of your OpenAPI" echo -e "spec to the steady command:" echo - echo -e " \$ ${YELLOW}npm exec --package=@stdy/cli@0.20.1 -- steady path/to/your.openapi.yml --host 127.0.0.1 -p 4010 --validator-query-array-format=comma --validator-form-array-format=comma --validator-query-object-format=brackets --validator-form-object-format=brackets${NC}" + echo -e " \$ ${YELLOW}npm exec --package=@stdy/cli@0.20.2 -- steady path/to/your.openapi.yml --host 127.0.0.1 -p 4010 --validator-query-array-format=comma --validator-form-array-format=comma --validator-query-object-format=brackets --validator-form-object-format=brackets${NC}" echo exit 1 From da4adf593e5ce6f504bda6a2f861bfb17eb2ac79 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 6 Apr 2026 16:54:47 +0000 Subject: [PATCH 289/330] feat: [CORE-000][apps/api] Add verified to SDK --- .stats.yml | 4 ++-- src/browserbase/types/session_create_params.py | 3 +++ tests/api_resources/test_sessions.py | 2 ++ 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index 96c76817..bf216680 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 21 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fbrowserbase-27e5ad306f9f631a025e0e4047ac1b2d1e7674fa533f99800aa83eb68a19c98b.yml -openapi_spec_hash: eb6e27e1aa8c906d8c8b007830ec7e02 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fbrowserbase-921d3c61c7aa06269f74bee63cee993597944f913429caa2aa2e00dd51fab60f.yml +openapi_spec_hash: d35b9613c41bf172fa2b28aceef10b39 config_hash: cf04ecfb8dad5fbd8b85be25d6e9ec55 diff --git a/src/browserbase/types/session_create_params.py b/src/browserbase/types/session_create_params.py index 63805590..c7180636 100644 --- a/src/browserbase/types/session_create_params.py +++ b/src/browserbase/types/session_create_params.py @@ -123,6 +123,9 @@ class BrowserSettings(TypedDict, total=False): solve_captchas: Annotated[bool, PropertyInfo(alias="solveCaptchas")] """Enable or disable captcha solving in the browser. Defaults to `true`.""" + verified: bool + """Verified Browser Mode""" + viewport: BrowserSettingsViewport diff --git a/tests/api_resources/test_sessions.py b/tests/api_resources/test_sessions.py index d237069c..eb07d3fc 100644 --- a/tests/api_resources/test_sessions.py +++ b/tests/api_resources/test_sessions.py @@ -45,6 +45,7 @@ def test_method_create_with_all_params(self, client: Browserbase) -> None: "os": "windows", "record_session": True, "solve_captchas": True, + "verified": True, "viewport": { "height": 0, "width": 0, @@ -278,6 +279,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncBrowserbas "os": "windows", "record_session": True, "solve_captchas": True, + "verified": True, "viewport": { "height": 0, "width": 0, From 5567941c45ba45294ce7a4960f5c42737ac592f7 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 6 Apr 2026 19:31:25 +0000 Subject: [PATCH 290/330] chore(internal): version bump --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/browserbase/_version.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index cce9d1c6..c523ce19 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "1.7.0" + ".": "1.8.0" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 5b3df704..c9f36c78 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "browserbase" -version = "1.7.0" +version = "1.8.0" description = "The official Python library for the Browserbase API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/browserbase/_version.py b/src/browserbase/_version.py index 1b8456be..10594de4 100644 --- a/src/browserbase/_version.py +++ b/src/browserbase/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "browserbase" -__version__ = "1.7.0" # x-release-please-version +__version__ = "1.8.0" # x-release-please-version From ebec285ac5f49a8d0c0dfe4a19c2afb06dce6159 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 6 Apr 2026 22:55:54 +0000 Subject: [PATCH 291/330] feat: [CORE-1928][apps/api] Add `PENDING` as a valid session state --- .stats.yml | 4 ++-- src/browserbase/resources/sessions/sessions.py | 4 ++-- src/browserbase/types/session.py | 2 +- src/browserbase/types/session_list_params.py | 2 +- tests/api_resources/test_sessions.py | 4 ++-- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.stats.yml b/.stats.yml index bf216680..cfabd9dc 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 21 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fbrowserbase-921d3c61c7aa06269f74bee63cee993597944f913429caa2aa2e00dd51fab60f.yml -openapi_spec_hash: d35b9613c41bf172fa2b28aceef10b39 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fbrowserbase-592ec7680e78e2cb6f33a051cb82c208b93c174b7458186efb54fca8254312d1.yml +openapi_spec_hash: 77b58db061531c44f27d9bd5fbff9e93 config_hash: cf04ecfb8dad5fbd8b85be25d6e9ec55 diff --git a/src/browserbase/resources/sessions/sessions.py b/src/browserbase/resources/sessions/sessions.py index 18bb0a73..ce3de98f 100644 --- a/src/browserbase/resources/sessions/sessions.py +++ b/src/browserbase/resources/sessions/sessions.py @@ -255,7 +255,7 @@ def list( self, *, q: str | Omit = omit, - status: Literal["RUNNING", "ERROR", "TIMED_OUT", "COMPLETED"] | Omit = omit, + status: Literal["PENDING", "RUNNING", "ERROR", "TIMED_OUT", "COMPLETED"] | 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, @@ -527,7 +527,7 @@ async def list( self, *, q: str | Omit = omit, - status: Literal["RUNNING", "ERROR", "TIMED_OUT", "COMPLETED"] | Omit = omit, + status: Literal["PENDING", "RUNNING", "ERROR", "TIMED_OUT", "COMPLETED"] | 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, diff --git a/src/browserbase/types/session.py b/src/browserbase/types/session.py index e983baaa..eac6a815 100644 --- a/src/browserbase/types/session.py +++ b/src/browserbase/types/session.py @@ -32,7 +32,7 @@ class Session(BaseModel): started_at: datetime = FieldInfo(alias="startedAt") - status: Literal["RUNNING", "ERROR", "TIMED_OUT", "COMPLETED"] + status: Literal["PENDING", "RUNNING", "ERROR", "TIMED_OUT", "COMPLETED"] updated_at: datetime = FieldInfo(alias="updatedAt") diff --git a/src/browserbase/types/session_list_params.py b/src/browserbase/types/session_list_params.py index 54b0a05c..c21b98e1 100644 --- a/src/browserbase/types/session_list_params.py +++ b/src/browserbase/types/session_list_params.py @@ -16,4 +16,4 @@ class SessionListParams(TypedDict, total=False): for the schema of this query. """ - status: Literal["RUNNING", "ERROR", "TIMED_OUT", "COMPLETED"] + status: Literal["PENDING", "RUNNING", "ERROR", "TIMED_OUT", "COMPLETED"] diff --git a/tests/api_resources/test_sessions.py b/tests/api_resources/test_sessions.py index eb07d3fc..78f99ceb 100644 --- a/tests/api_resources/test_sessions.py +++ b/tests/api_resources/test_sessions.py @@ -189,7 +189,7 @@ def test_method_list(self, client: Browserbase) -> None: def test_method_list_with_all_params(self, client: Browserbase) -> None: session = client.sessions.list( q="q", - status="RUNNING", + status="PENDING", ) assert_matches_type(SessionListResponse, session, path=["response"]) @@ -423,7 +423,7 @@ async def test_method_list(self, async_client: AsyncBrowserbase) -> None: async def test_method_list_with_all_params(self, async_client: AsyncBrowserbase) -> None: session = await async_client.sessions.list( q="q", - status="RUNNING", + status="PENDING", ) assert_matches_type(SessionListResponse, session, path=["response"]) From ef23338aad9c88cc751b16cdd7854b081b7e7dd1 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 8 Apr 2026 02:15:01 +0000 Subject: [PATCH 292/330] fix(client): preserve hardcoded query params when merging with user params --- src/browserbase/_base_client.py | 4 +++ tests/test_client.py | 48 +++++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+) diff --git a/src/browserbase/_base_client.py b/src/browserbase/_base_client.py index 5bc9823d..bd88f594 100644 --- a/src/browserbase/_base_client.py +++ b/src/browserbase/_base_client.py @@ -540,6 +540,10 @@ def _build_request( files = cast(HttpxRequestFiles, ForceMultipartDict()) prepared_url = self._prepare_url(options.url) + # preserve hard-coded query params from the url + if params and prepared_url.query: + params = {**dict(prepared_url.params.items()), **params} + prepared_url = prepared_url.copy_with(raw_path=prepared_url.raw_path.split(b"?", 1)[0]) if "_" in prepared_url.host: # work around https://github.com/encode/httpx/discussions/2880 kwargs["extensions"] = {"sni_hostname": prepared_url.host.replace("_", "-")} diff --git a/tests/test_client.py b/tests/test_client.py index 1d0d68b3..95ae8e03 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -429,6 +429,30 @@ def test_default_query_option(self) -> None: client.close() + def test_hardcoded_query_params_in_url(self, client: Browserbase) -> None: + request = client._build_request(FinalRequestOptions(method="get", url="/foo?beta=true")) + url = httpx.URL(request.url) + assert dict(url.params) == {"beta": "true"} + + request = client._build_request( + FinalRequestOptions( + method="get", + url="/foo?beta=true", + params={"limit": "10", "page": "abc"}, + ) + ) + url = httpx.URL(request.url) + assert dict(url.params) == {"beta": "true", "limit": "10", "page": "abc"} + + request = client._build_request( + FinalRequestOptions( + method="get", + url="/files/a%2Fb?beta=true", + params={"limit": "10"}, + ) + ) + assert request.url.raw_path == b"/files/a%2Fb?beta=true&limit=10" + def test_request_extra_json(self, client: Browserbase) -> None: request = client._build_request( FinalRequestOptions( @@ -1330,6 +1354,30 @@ async def test_default_query_option(self) -> None: await client.close() + async def test_hardcoded_query_params_in_url(self, async_client: AsyncBrowserbase) -> None: + request = async_client._build_request(FinalRequestOptions(method="get", url="/foo?beta=true")) + url = httpx.URL(request.url) + assert dict(url.params) == {"beta": "true"} + + request = async_client._build_request( + FinalRequestOptions( + method="get", + url="/foo?beta=true", + params={"limit": "10", "page": "abc"}, + ) + ) + url = httpx.URL(request.url) + assert dict(url.params) == {"beta": "true", "limit": "10", "page": "abc"} + + request = async_client._build_request( + FinalRequestOptions( + method="get", + url="/files/a%2Fb?beta=true", + params={"limit": "10"}, + ) + ) + assert request.url.raw_path == b"/files/a%2Fb?beta=true&limit=10" + def test_request_extra_json(self, client: Browserbase) -> None: request = client._build_request( FinalRequestOptions( From fcba4772874970d69d7f13a0942289752f07fd17 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 8 Apr 2026 21:22:45 +0000 Subject: [PATCH 293/330] codegen metadata --- .stats.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index cfabd9dc..c754d3fa 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 21 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fbrowserbase-592ec7680e78e2cb6f33a051cb82c208b93c174b7458186efb54fca8254312d1.yml -openapi_spec_hash: 77b58db061531c44f27d9bd5fbff9e93 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fbrowserbase-a4cf3ac2380d1e1625bac730a8d396960f0d12599ad630355c38b33eeb2ef10a.yml +openapi_spec_hash: 79afe4d9341a697c9277596db045155b config_hash: cf04ecfb8dad5fbd8b85be25d6e9ec55 From 70a035f1ea23ede13b9b1e997b03e8b231dcf48b Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 8 Apr 2026 23:15:46 +0000 Subject: [PATCH 294/330] codegen metadata --- .stats.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index c754d3fa..b1784cf2 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 21 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fbrowserbase-a4cf3ac2380d1e1625bac730a8d396960f0d12599ad630355c38b33eeb2ef10a.yml -openapi_spec_hash: 79afe4d9341a697c9277596db045155b +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fbrowserbase-3683b291883198719787c333144da6650b6c287db400e0b21f54797f6e986a24.yml +openapi_spec_hash: 4da0b34a056487d20ed56a3b0b1c078e config_hash: cf04ecfb8dad5fbd8b85be25d6e9ec55 From fa97033a18b0a43b59df6687bbd6d67fec734f0d Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 11 Apr 2026 02:38:14 +0000 Subject: [PATCH 295/330] fix: ensure file data are only sent as 1 parameter --- src/browserbase/_utils/_utils.py | 5 +++-- tests/test_extract_files.py | 9 +++++++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/browserbase/_utils/_utils.py b/src/browserbase/_utils/_utils.py index eec7f4a1..63b8cd60 100644 --- a/src/browserbase/_utils/_utils.py +++ b/src/browserbase/_utils/_utils.py @@ -86,8 +86,9 @@ def _extract_items( 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: + # Remove the field if there are no more dict keys in the path, + # only "" traversal markers or end. + if all(p == "" for p in path[index:]): item = obj.pop(key) else: item = obj[key] diff --git a/tests/test_extract_files.py b/tests/test_extract_files.py index 3c0fcb36..cd614d03 100644 --- a/tests/test_extract_files.py +++ b/tests/test_extract_files.py @@ -35,6 +35,15 @@ def test_multiple_files() -> None: assert query == {"documents": [{}, {}]} +def test_top_level_file_array() -> None: + query = {"files": [b"file one", b"file two"], "title": "hello"} + assert extract_files(query, paths=[["files", ""]]) == [ + ("files[]", b"file one"), + ("files[]", b"file two"), + ] + assert query == {"title": "hello"} + + @pytest.mark.parametrize( "query,paths,expected", [ From 4163219045fd800c351d90f52e02d0efc4cb4c0b Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 18 Apr 2026 02:37:39 +0000 Subject: [PATCH 296/330] perf(client): optimize file structure copying in multipart requests --- src/browserbase/_files.py | 56 ++++++++++- src/browserbase/_utils/__init__.py | 1 - src/browserbase/_utils/_utils.py | 15 --- src/browserbase/resources/extensions.py | 7 +- src/browserbase/resources/sessions/uploads.py | 7 +- tests/test_deepcopy.py | 58 ----------- tests/test_files.py | 99 ++++++++++++++++++- 7 files changed, 159 insertions(+), 84 deletions(-) delete mode 100644 tests/test_deepcopy.py diff --git a/src/browserbase/_files.py b/src/browserbase/_files.py index ff951be7..83e91f60 100644 --- a/src/browserbase/_files.py +++ b/src/browserbase/_files.py @@ -3,8 +3,8 @@ import io import os import pathlib -from typing import overload -from typing_extensions import TypeGuard +from typing import Sequence, cast, overload +from typing_extensions import TypeVar, TypeGuard import anyio @@ -17,7 +17,9 @@ HttpxFileContent, HttpxRequestFiles, ) -from ._utils import is_tuple_t, is_mapping_t, is_sequence_t +from ._utils import is_list, is_mapping, is_tuple_t, is_mapping_t, is_sequence_t + +_T = TypeVar("_T") def is_base64_file_input(obj: object) -> TypeGuard[Base64FileInput]: @@ -121,3 +123,51 @@ async def async_read_file_content(file: FileContent) -> HttpxFileContent: return await anyio.Path(file).read_bytes() return file + + +def deepcopy_with_paths(item: _T, paths: Sequence[Sequence[str]]) -> _T: + """Copy only the containers along the given paths. + + Used to guard against mutation by extract_files without copying the entire structure. + Only dicts and lists that lie on a path are copied; everything else + is returned by reference. + + For example, given paths=[["foo", "files", "file"]] and the structure: + { + "foo": { + "bar": {"baz": {}}, + "files": {"file": } + } + } + The root dict, "foo", and "files" are copied (they lie on the path). + "bar" and "baz" are returned by reference (off the path). + """ + return _deepcopy_with_paths(item, paths, 0) + + +def _deepcopy_with_paths(item: _T, paths: Sequence[Sequence[str]], index: int) -> _T: + if not paths: + return item + if is_mapping(item): + key_to_paths: dict[str, list[Sequence[str]]] = {} + for path in paths: + if index < len(path): + key_to_paths.setdefault(path[index], []).append(path) + + # if no path continues through this mapping, it won't be mutated and copying it is redundant + if not key_to_paths: + return item + + result = dict(item) + for key, subpaths in key_to_paths.items(): + if key in result: + result[key] = _deepcopy_with_paths(result[key], subpaths, index + 1) + return cast(_T, result) + if is_list(item): + array_paths = [path for path in paths if index < len(path) and path[index] == ""] + + # if no path expects a list here, nothing will be mutated inside it - return by reference + if not array_paths: + return cast(_T, item) + return cast(_T, [_deepcopy_with_paths(entry, array_paths, index + 1) for entry in item]) + return item diff --git a/src/browserbase/_utils/__init__.py b/src/browserbase/_utils/__init__.py index 10cb66d2..1c090e51 100644 --- a/src/browserbase/_utils/__init__.py +++ b/src/browserbase/_utils/__init__.py @@ -24,7 +24,6 @@ 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, diff --git a/src/browserbase/_utils/_utils.py b/src/browserbase/_utils/_utils.py index 63b8cd60..771859f5 100644 --- a/src/browserbase/_utils/_utils.py +++ b/src/browserbase/_utils/_utils.py @@ -177,21 +177,6 @@ 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) diff --git a/src/browserbase/resources/extensions.py b/src/browserbase/resources/extensions.py index 2d6fb1b0..9325d6b6 100644 --- a/src/browserbase/resources/extensions.py +++ b/src/browserbase/resources/extensions.py @@ -7,8 +7,9 @@ import httpx from ..types import extension_create_params +from .._files import deepcopy_with_paths from .._types import Body, Query, Headers, NoneType, NotGiven, FileTypes, not_given -from .._utils import extract_files, path_template, maybe_transform, deepcopy_minimal, async_maybe_transform +from .._utils import extract_files, path_template, maybe_transform, async_maybe_transform from .._compat import cached_property from .._resource import SyncAPIResource, AsyncAPIResource from .._response import ( @@ -66,7 +67,7 @@ def create( timeout: Override the client-level default timeout for this request, in seconds """ - body = deepcopy_minimal({"file": file}) + body = deepcopy_with_paths({"file": file}, [["file"]]) files = extract_files(cast(Mapping[str, object], body), paths=[["file"]]) # It should be noted that the actual Content-Type header that will be # sent to the server will contain a `boundary` parameter, e.g. @@ -193,7 +194,7 @@ async def create( timeout: Override the client-level default timeout for this request, in seconds """ - body = deepcopy_minimal({"file": file}) + body = deepcopy_with_paths({"file": file}, [["file"]]) files = extract_files(cast(Mapping[str, object], body), paths=[["file"]]) # It should be noted that the actual Content-Type header that will be # sent to the server will contain a `boundary` parameter, e.g. diff --git a/src/browserbase/resources/sessions/uploads.py b/src/browserbase/resources/sessions/uploads.py index 7c776029..f5d22d96 100644 --- a/src/browserbase/resources/sessions/uploads.py +++ b/src/browserbase/resources/sessions/uploads.py @@ -6,8 +6,9 @@ import httpx +from ..._files import deepcopy_with_paths from ..._types import Body, Query, Headers, NotGiven, FileTypes, not_given -from ..._utils import extract_files, path_template, maybe_transform, deepcopy_minimal, async_maybe_transform +from ..._utils import extract_files, path_template, maybe_transform, async_maybe_transform from ..._compat import cached_property from ..._resource import SyncAPIResource, AsyncAPIResource from ..._response import ( @@ -69,7 +70,7 @@ def create( """ if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") - body = deepcopy_minimal({"file": file}) + body = deepcopy_with_paths({"file": file}, [["file"]]) files = extract_files(cast(Mapping[str, object], body), paths=[["file"]]) # It should be noted that the actual Content-Type header that will be # sent to the server will contain a `boundary` parameter, e.g. @@ -132,7 +133,7 @@ async def create( """ if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") - body = deepcopy_minimal({"file": file}) + body = deepcopy_with_paths({"file": file}, [["file"]]) files = extract_files(cast(Mapping[str, object], body), paths=[["file"]]) # It should be noted that the actual Content-Type header that will be # sent to the server will contain a `boundary` parameter, e.g. diff --git a/tests/test_deepcopy.py b/tests/test_deepcopy.py deleted file mode 100644 index a2a29e38..00000000 --- a/tests/test_deepcopy.py +++ /dev/null @@ -1,58 +0,0 @@ -from browserbase._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_files.py b/tests/test_files.py index d8842d61..d1ccc25f 100644 --- a/tests/test_files.py +++ b/tests/test_files.py @@ -4,7 +4,8 @@ import pytest from dirty_equals import IsDict, IsList, IsBytes, IsTuple -from browserbase._files import to_httpx_files, async_to_httpx_files +from browserbase._files import to_httpx_files, deepcopy_with_paths, async_to_httpx_files +from browserbase._utils import extract_files readme_path = Path(__file__).parent.parent.joinpath("README.md") @@ -49,3 +50,99 @@ def test_string_not_allowed() -> None: "file": "foo", # type: ignore } ) + + +def assert_different_identities(obj1: object, obj2: object) -> None: + assert obj1 == obj2 + assert obj1 is not obj2 + + +class TestDeepcopyWithPaths: + def test_copies_top_level_dict(self) -> None: + original = {"file": b"data", "other": "value"} + result = deepcopy_with_paths(original, [["file"]]) + assert_different_identities(result, original) + + def test_file_value_is_same_reference(self) -> None: + file_bytes = b"contents" + original = {"file": file_bytes} + result = deepcopy_with_paths(original, [["file"]]) + assert_different_identities(result, original) + assert result["file"] is file_bytes + + def test_list_popped_wholesale(self) -> None: + files = [b"f1", b"f2"] + original = {"files": files, "title": "t"} + result = deepcopy_with_paths(original, [["files", ""]]) + assert_different_identities(result, original) + result_files = result["files"] + assert isinstance(result_files, list) + assert_different_identities(result_files, files) + + def test_nested_array_path_copies_list_and_elements(self) -> None: + elem1 = {"file": b"f1", "extra": 1} + elem2 = {"file": b"f2", "extra": 2} + original = {"items": [elem1, elem2]} + result = deepcopy_with_paths(original, [["items", "", "file"]]) + assert_different_identities(result, original) + result_items = result["items"] + assert isinstance(result_items, list) + assert_different_identities(result_items, original["items"]) + assert_different_identities(result_items[0], elem1) + assert_different_identities(result_items[1], elem2) + + def test_empty_paths_returns_same_object(self) -> None: + original = {"foo": "bar"} + result = deepcopy_with_paths(original, []) + assert result is original + + def test_multiple_paths(self) -> None: + f1 = b"file1" + f2 = b"file2" + original = {"a": f1, "b": f2, "c": "unchanged"} + result = deepcopy_with_paths(original, [["a"], ["b"]]) + assert_different_identities(result, original) + assert result["a"] is f1 + assert result["b"] is f2 + assert result["c"] is original["c"] + + def test_extract_files_does_not_mutate_original_top_level(self) -> None: + file_bytes = b"contents" + original = {"file": file_bytes, "other": "value"} + + copied = deepcopy_with_paths(original, [["file"]]) + extracted = extract_files(copied, paths=[["file"]]) + + assert extracted == [("file", file_bytes)] + assert original == {"file": file_bytes, "other": "value"} + assert copied == {"other": "value"} + + def test_extract_files_does_not_mutate_original_nested_array_path(self) -> None: + file1 = b"f1" + file2 = b"f2" + original = { + "items": [ + {"file": file1, "extra": 1}, + {"file": file2, "extra": 2}, + ], + "title": "example", + } + + copied = deepcopy_with_paths(original, [["items", "", "file"]]) + extracted = extract_files(copied, paths=[["items", "", "file"]]) + + assert extracted == [("items[][file]", file1), ("items[][file]", file2)] + assert original == { + "items": [ + {"file": file1, "extra": 1}, + {"file": file2, "extra": 2}, + ], + "title": "example", + } + assert copied == { + "items": [ + {"extra": 1}, + {"extra": 2}, + ], + "title": "example", + } From 2fc2827688c2787524060ff6e50aa5b538e30c29 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 18 Apr 2026 02:39:24 +0000 Subject: [PATCH 297/330] chore(tests): bump steady to v0.22.1 --- scripts/mock | 6 +++--- scripts/test | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/scripts/mock b/scripts/mock index 5cd7c157..feebe5ed 100755 --- a/scripts/mock +++ b/scripts/mock @@ -22,9 +22,9 @@ echo "==> Starting mock server with URL ${URL}" # Run steady mock on the given spec if [ "$1" == "--daemon" ]; then # Pre-install the package so the download doesn't eat into the startup timeout - npm exec --package=@stdy/cli@0.20.2 -- steady --version + npm exec --package=@stdy/cli@0.22.1 -- steady --version - npm exec --package=@stdy/cli@0.20.2 -- steady --host 127.0.0.1 -p 4010 --validator-query-array-format=comma --validator-form-array-format=comma --validator-query-object-format=brackets --validator-form-object-format=brackets "$URL" &> .stdy.log & + npm exec --package=@stdy/cli@0.22.1 -- steady --host 127.0.0.1 -p 4010 --validator-query-array-format=comma --validator-form-array-format=comma --validator-query-object-format=brackets --validator-form-object-format=brackets "$URL" &> .stdy.log & # Wait for server to come online via health endpoint (max 30s) echo -n "Waiting for server" @@ -48,5 +48,5 @@ if [ "$1" == "--daemon" ]; then echo else - npm exec --package=@stdy/cli@0.20.2 -- steady --host 127.0.0.1 -p 4010 --validator-query-array-format=comma --validator-form-array-format=comma --validator-query-object-format=brackets --validator-form-object-format=brackets "$URL" + npm exec --package=@stdy/cli@0.22.1 -- steady --host 127.0.0.1 -p 4010 --validator-query-array-format=comma --validator-form-array-format=comma --validator-query-object-format=brackets --validator-form-object-format=brackets "$URL" fi diff --git a/scripts/test b/scripts/test index b8143aa3..19acc916 100755 --- a/scripts/test +++ b/scripts/test @@ -43,7 +43,7 @@ elif ! steady_is_running ; then echo -e "To run the server, pass in the path or url of your OpenAPI" echo -e "spec to the steady command:" echo - echo -e " \$ ${YELLOW}npm exec --package=@stdy/cli@0.20.2 -- steady path/to/your.openapi.yml --host 127.0.0.1 -p 4010 --validator-query-array-format=comma --validator-form-array-format=comma --validator-query-object-format=brackets --validator-form-object-format=brackets${NC}" + echo -e " \$ ${YELLOW}npm exec --package=@stdy/cli@0.22.1 -- steady path/to/your.openapi.yml --host 127.0.0.1 -p 4010 --validator-query-array-format=comma --validator-form-array-format=comma --validator-query-object-format=brackets --validator-form-object-format=brackets${NC}" echo exit 1 From ec7bbbab919e068e9be8a01011613bbe662ba1bd Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 23 Apr 2026 02:13:08 +0000 Subject: [PATCH 298/330] chore(internal): more robust bootstrap script --- scripts/bootstrap | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/bootstrap b/scripts/bootstrap index b430fee3..fe8451e4 100755 --- a/scripts/bootstrap +++ b/scripts/bootstrap @@ -4,7 +4,7 @@ set -e cd "$(dirname "$0")/.." -if [ -f "Brewfile" ] && [ "$(uname -s)" = "Darwin" ] && [ "$SKIP_BREW" != "1" ] && [ -t 0 ]; then +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 From e0d27c2a8ef7180b1c013fe1604049c644d38550 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 24 Apr 2026 17:57:29 +0000 Subject: [PATCH 299/330] feat: [CORE-1979] [apps/api] Regenerate OpenAPI spec to match current routes --- .stats.yml | 4 ++-- src/browserbase/types/session_create_params.py | 6 ++++++ tests/api_resources/test_sessions.py | 2 ++ 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index b1784cf2..e19cfd64 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 21 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fbrowserbase-3683b291883198719787c333144da6650b6c287db400e0b21f54797f6e986a24.yml -openapi_spec_hash: 4da0b34a056487d20ed56a3b0b1c078e +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fbrowserbase-16de8c0da31c4edcda2c2344f6bbad71aa803869b017bbfdd715afa309d113ba.yml +openapi_spec_hash: 40adcceb64631c781311492ba14e8c8f config_hash: cf04ecfb8dad5fbd8b85be25d6e9ec55 diff --git a/src/browserbase/types/session_create_params.py b/src/browserbase/types/session_create_params.py index c7180636..3ff1d058 100644 --- a/src/browserbase/types/session_create_params.py +++ b/src/browserbase/types/session_create_params.py @@ -108,6 +108,12 @@ class BrowserSettings(TypedDict, total=False): See [Upload Extension](/reference/api/upload-an-extension). """ + ignore_certificate_errors: Annotated[bool, PropertyInfo(alias="ignoreCertificateErrors")] + """Enable or disable ignoring of certificate errors in the browser. + + Defaults to `true`. + """ + log_session: Annotated[bool, PropertyInfo(alias="logSession")] """Enable or disable session logging. Defaults to `true`.""" diff --git a/tests/api_resources/test_sessions.py b/tests/api_resources/test_sessions.py index 78f99ceb..41f2a0bd 100644 --- a/tests/api_resources/test_sessions.py +++ b/tests/api_resources/test_sessions.py @@ -41,6 +41,7 @@ def test_method_create_with_all_params(self, client: Browserbase) -> None: "persist": True, }, "extension_id": "extensionId", + "ignore_certificate_errors": True, "log_session": True, "os": "windows", "record_session": True, @@ -275,6 +276,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncBrowserbas "persist": True, }, "extension_id": "extensionId", + "ignore_certificate_errors": True, "log_session": True, "os": "windows", "record_session": True, From cc99d51d8b892c4eead28e6df2a5aa16477e7d8d Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 28 Apr 2026 02:06:27 +0000 Subject: [PATCH 300/330] fix: use correct field name format for multipart file arrays --- src/browserbase/_qs.py | 8 ++---- src/browserbase/_types.py | 3 +++ src/browserbase/_utils/_utils.py | 42 ++++++++++++++++++++++++++------ tests/test_extract_files.py | 28 +++++++++++++++++---- tests/test_files.py | 2 +- 5 files changed, 63 insertions(+), 20 deletions(-) diff --git a/src/browserbase/_qs.py b/src/browserbase/_qs.py index de8c99bc..4127c19c 100644 --- a/src/browserbase/_qs.py +++ b/src/browserbase/_qs.py @@ -2,17 +2,13 @@ from typing import Any, List, Tuple, Union, Mapping, TypeVar from urllib.parse import parse_qs, urlencode -from typing_extensions import Literal, get_args +from typing_extensions import get_args -from ._types import NotGiven, not_given +from ._types import NotGiven, ArrayFormat, NestedFormat, 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 diff --git a/src/browserbase/_types.py b/src/browserbase/_types.py index abefae08..b2ac81be 100644 --- a/src/browserbase/_types.py +++ b/src/browserbase/_types.py @@ -47,6 +47,9 @@ ModelT = TypeVar("ModelT", bound=pydantic.BaseModel) _T = TypeVar("_T") +ArrayFormat = Literal["comma", "repeat", "indices", "brackets"] +NestedFormat = Literal["dots", "brackets"] + # Approximates httpx internal ProxiesTypes and RequestFiles types # while adding support for `PathLike` instances diff --git a/src/browserbase/_utils/_utils.py b/src/browserbase/_utils/_utils.py index 771859f5..199cd231 100644 --- a/src/browserbase/_utils/_utils.py +++ b/src/browserbase/_utils/_utils.py @@ -17,11 +17,11 @@ ) from pathlib import Path from datetime import date, datetime -from typing_extensions import TypeGuard +from typing_extensions import TypeGuard, get_args import sniffio -from .._types import Omit, NotGiven, FileTypes, HeadersLike +from .._types import Omit, NotGiven, FileTypes, ArrayFormat, HeadersLike _T = TypeVar("_T") _TupleT = TypeVar("_TupleT", bound=Tuple[object, ...]) @@ -40,25 +40,45 @@ def extract_files( query: Mapping[str, object], *, paths: Sequence[Sequence[str]], + array_format: ArrayFormat = "brackets", ) -> list[tuple[str, FileTypes]]: """Recursively extract files from the given dictionary based on specified paths. A path may look like this ['foo', 'files', '', 'data']. + ``array_format`` controls how ```` segments contribute to the emitted + field name. Supported values: ``"brackets"`` (``foo[]``), ``"repeat"`` and + ``"comma"`` (``foo``), ``"indices"`` (``foo[0]``, ``foo[1]``). + 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)) + files.extend(_extract_items(query, path, index=0, flattened_key=None, array_format=array_format)) return files +def _array_suffix(array_format: ArrayFormat, array_index: int) -> str: + if array_format == "brackets": + return "[]" + if array_format == "indices": + return f"[{array_index}]" + if array_format == "repeat" or array_format == "comma": + # Both repeat the bare field name for each file part; there is no + # meaningful way to comma-join binary parts. + return "" + raise NotImplementedError( + f"Unknown array_format value: {array_format}, choose from {', '.join(get_args(ArrayFormat))}" + ) + + def _extract_items( obj: object, path: Sequence[str], *, index: int, flattened_key: str | None, + array_format: ArrayFormat, ) -> list[tuple[str, FileTypes]]: try: key = path[index] @@ -75,9 +95,11 @@ def _extract_items( 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))) + for array_index, entry in enumerate(obj): + suffix = _array_suffix(array_format, array_index) + emitted_key = (flattened_key + suffix) if flattened_key else suffix + assert_is_file_content(entry, key=emitted_key) + files.append((emitted_key, cast(FileTypes, entry))) return files assert_is_file_content(obj, key=flattened_key) @@ -106,6 +128,7 @@ def _extract_items( path, index=index, flattened_key=flattened_key, + array_format=array_format, ) elif is_list(obj): if key != "": @@ -117,9 +140,12 @@ def _extract_items( item, path, index=index, - flattened_key=flattened_key + "[]" if flattened_key is not None else "[]", + flattened_key=( + (flattened_key if flattened_key is not None else "") + _array_suffix(array_format, array_index) + ), + array_format=array_format, ) - for item in obj + for array_index, item in enumerate(obj) ] ) diff --git a/tests/test_extract_files.py b/tests/test_extract_files.py index cd614d03..07deeb6d 100644 --- a/tests/test_extract_files.py +++ b/tests/test_extract_files.py @@ -4,7 +4,7 @@ import pytest -from browserbase._types import FileTypes +from browserbase._types import FileTypes, ArrayFormat from browserbase._utils import extract_files @@ -37,10 +37,7 @@ def test_multiple_files() -> None: def test_top_level_file_array() -> None: query = {"files": [b"file one", b"file two"], "title": "hello"} - assert extract_files(query, paths=[["files", ""]]) == [ - ("files[]", b"file one"), - ("files[]", b"file two"), - ] + assert extract_files(query, paths=[["files", ""]]) == [("files[]", b"file one"), ("files[]", b"file two")] assert query == {"title": "hello"} @@ -71,3 +68,24 @@ def test_ignores_incorrect_paths( expected: list[tuple[str, FileTypes]], ) -> None: assert extract_files(query, paths=paths) == expected + + +@pytest.mark.parametrize( + "array_format,expected_top_level,expected_nested", + [ + ("brackets", [("files[]", b"a"), ("files[]", b"b")], [("items[][file]", b"a"), ("items[][file]", b"b")]), + ("repeat", [("files", b"a"), ("files", b"b")], [("items[file]", b"a"), ("items[file]", b"b")]), + ("comma", [("files", b"a"), ("files", b"b")], [("items[file]", b"a"), ("items[file]", b"b")]), + ("indices", [("files[0]", b"a"), ("files[1]", b"b")], [("items[0][file]", b"a"), ("items[1][file]", b"b")]), + ], +) +def test_array_format_controls_file_field_names( + array_format: ArrayFormat, + expected_top_level: list[tuple[str, FileTypes]], + expected_nested: list[tuple[str, FileTypes]], +) -> None: + top_level = {"files": [b"a", b"b"]} + assert extract_files(top_level, paths=[["files", ""]], array_format=array_format) == expected_top_level + + nested = {"items": [{"file": b"a"}, {"file": b"b"}]} + assert extract_files(nested, paths=[["items", "", "file"]], array_format=array_format) == expected_nested diff --git a/tests/test_files.py b/tests/test_files.py index d1ccc25f..713c5994 100644 --- a/tests/test_files.py +++ b/tests/test_files.py @@ -131,7 +131,7 @@ def test_extract_files_does_not_mutate_original_nested_array_path(self) -> None: copied = deepcopy_with_paths(original, [["items", "", "file"]]) extracted = extract_files(copied, paths=[["items", "", "file"]]) - assert extracted == [("items[][file]", file1), ("items[][file]", file2)] + assert [entry for _, entry in extracted] == [file1, file2] assert original == { "items": [ {"file": file1, "extra": 1}, From 3bbcd634cdaf385bf862e1105e31b522df621627 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 28 Apr 2026 02:07:34 +0000 Subject: [PATCH 301/330] feat: support setting headers via env --- src/browserbase/_client.py | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/src/browserbase/_client.py b/src/browserbase/_client.py index d642273a..b64caef8 100644 --- a/src/browserbase/_client.py +++ b/src/browserbase/_client.py @@ -19,7 +19,11 @@ RequestOptions, not_given, ) -from ._utils import is_given, get_async_library +from ._utils import ( + is_given, + is_mapping_t, + get_async_library, +) from ._compat import cached_property from ._version import __version__ from ._streaming import Stream as Stream, AsyncStream as AsyncStream @@ -95,6 +99,15 @@ def __init__( if base_url is None: base_url = f"https://api.browserbase.com" + custom_headers_env = os.environ.get("BROWSERBASE_CUSTOM_HEADERS") + if custom_headers_env is not None: + parsed: dict[str, str] = {} + for line in custom_headers_env.split("\n"): + colon = line.find(":") + if colon >= 0: + parsed[line[:colon].strip()] = line[colon + 1 :].strip() + default_headers = {**parsed, **(default_headers if is_mapping_t(default_headers) else {})} + super().__init__( version=__version__, base_url=base_url, @@ -299,6 +312,15 @@ def __init__( if base_url is None: base_url = f"https://api.browserbase.com" + custom_headers_env = os.environ.get("BROWSERBASE_CUSTOM_HEADERS") + if custom_headers_env is not None: + parsed: dict[str, str] = {} + for line in custom_headers_env.split("\n"): + colon = line.find(":") + if colon >= 0: + parsed[line[:colon].strip()] = line[colon + 1 :].strip() + default_headers = {**parsed, **(default_headers if is_mapping_t(default_headers) else {})} + super().__init__( version=__version__, base_url=base_url, From 2b21910f18920d9dfd223a7713d65b3ec8fd7da3 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 30 Apr 2026 04:29:41 +0000 Subject: [PATCH 302/330] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index e19cfd64..1f8b4432 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 21 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fbrowserbase-16de8c0da31c4edcda2c2344f6bbad71aa803869b017bbfdd715afa309d113ba.yml +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase/browserbase-16de8c0da31c4edcda2c2344f6bbad71aa803869b017bbfdd715afa309d113ba.yml openapi_spec_hash: 40adcceb64631c781311492ba14e8c8f config_hash: cf04ecfb8dad5fbd8b85be25d6e9ec55 From 891098f261ebb806c28d41b2c4489270306540bd Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 1 May 2026 03:35:38 +0000 Subject: [PATCH 303/330] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index 1f8b4432..e5d38fd8 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 21 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase/browserbase-16de8c0da31c4edcda2c2344f6bbad71aa803869b017bbfdd715afa309d113ba.yml +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase/browserbase-213a694dcb7548e9da9f79fdc415ae79475f51fed8353bc684b56bef5208e4ff.yml openapi_spec_hash: 40adcceb64631c781311492ba14e8c8f config_hash: cf04ecfb8dad5fbd8b85be25d6e9ec55 From 11f000f82513aadd66a04457ea608887cbc1572d Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 1 May 2026 03:39:28 +0000 Subject: [PATCH 304/330] chore(internal): reformat pyproject.toml --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index c9f36c78..e3027a02 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -168,7 +168,7 @@ show_error_codes = true # # We also exclude our `tests` as mypy doesn't always infer # types correctly and Pyright will still catch any type errors. -exclude = ['src/browserbase/_files.py', '_dev/.*.py', 'tests/.*'] +exclude = ["src/browserbase/_files.py", "_dev/.*.py", "tests/.*"] strict_equality = true implicit_reexport = true From 70f7c18565b176c6b0e0894e02ef786ac842115e Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 9 May 2026 03:18:09 +0000 Subject: [PATCH 305/330] fix(client): add missing f-string prefix in file type error message --- src/browserbase/_files.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/browserbase/_files.py b/src/browserbase/_files.py index 83e91f60..8042111f 100644 --- a/src/browserbase/_files.py +++ b/src/browserbase/_files.py @@ -99,7 +99,7 @@ async def async_to_httpx_files(files: RequestFiles | None) -> HttpxRequestFiles 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") + raise TypeError(f"Unexpected file type input {type(files)}, expected mapping or sequence") return files From 8da99f8305d5d656d6804bcff4de868a2ff61b30 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 12 May 2026 03:09:23 +0000 Subject: [PATCH 306/330] feat(internal/types): support eagerly validating pydantic iterators --- src/browserbase/_models.py | 80 ++++++++++++++++++++++++++++++++++++++ tests/test_models.py | 60 ++++++++++++++++++++++++++-- 2 files changed, 137 insertions(+), 3 deletions(-) diff --git a/src/browserbase/_models.py b/src/browserbase/_models.py index 29070e05..8c5ab260 100644 --- a/src/browserbase/_models.py +++ b/src/browserbase/_models.py @@ -25,7 +25,9 @@ ClassVar, Protocol, Required, + Annotated, ParamSpec, + TypeAlias, TypedDict, TypeGuard, final, @@ -79,7 +81,15 @@ from ._constants import RAW_RESPONSE_HEADER if TYPE_CHECKING: + from pydantic import GetCoreSchemaHandler, ValidatorFunctionWrapHandler + from pydantic_core import CoreSchema, core_schema from pydantic_core.core_schema import ModelField, ModelSchema, LiteralSchema, ModelFieldsSchema +else: + try: + from pydantic_core import CoreSchema, core_schema + except ImportError: + CoreSchema = None + core_schema = None __all__ = ["BaseModel", "GenericModel"] @@ -396,6 +406,76 @@ def model_dump_json( ) +class _EagerIterable(list[_T], Generic[_T]): + """ + Accepts any Iterable[T] input (including generators), consumes it + eagerly, and validates all items upfront. + + Validation preserves the original container type where possible + (e.g. a set[T] stays a set[T]). Serialization (model_dump / JSON) + always emits a list — round-tripping through model_dump() will not + restore the original container type. + """ + + @classmethod + def __get_pydantic_core_schema__( + cls, + source_type: Any, + handler: GetCoreSchemaHandler, + ) -> CoreSchema: + (item_type,) = get_args(source_type) or (Any,) + item_schema: CoreSchema = handler.generate_schema(item_type) + list_of_items_schema: CoreSchema = core_schema.list_schema(item_schema) + + return core_schema.no_info_wrap_validator_function( + cls._validate, + list_of_items_schema, + serialization=core_schema.plain_serializer_function_ser_schema( + cls._serialize, + info_arg=False, + ), + ) + + @staticmethod + def _validate(v: Iterable[_T], handler: "ValidatorFunctionWrapHandler") -> Any: + original_type: type[Any] = type(v) + + # Normalize to list so list_schema can validate each item + if isinstance(v, list): + items: list[_T] = v + else: + try: + items = list(v) + except TypeError as e: + raise TypeError("Value is not iterable") from e + + # Validate items against the inner schema + validated: list[_T] = handler(items) + + # Reconstruct original container type + if original_type is list: + return validated + # str(list) produces the list's repr, not a string built from items, + # so skip reconstruction for str and its subclasses. + if issubclass(original_type, str): + return validated + try: + return original_type(validated) + except (TypeError, ValueError): + # If the type cannot be reconstructed, just return the validated list + return validated + + @staticmethod + def _serialize(v: Iterable[_T]) -> list[_T]: + """Always serialize as a list so Pydantic's JSON encoder is happy.""" + if isinstance(v, list): + return v + return list(v) + + +EagerIterable: TypeAlias = Annotated[Iterable[_T], _EagerIterable] + + def _construct_field(value: object, field: FieldInfo, key: str) -> object: if value is None: return field_get_default(field) diff --git a/tests/test_models.py b/tests/test_models.py index 1ecdeecf..d65d819a 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -1,7 +1,8 @@ import json -from typing import TYPE_CHECKING, Any, Dict, List, Union, Optional, cast +from typing import TYPE_CHECKING, Any, Dict, List, Union, Iterable, Optional, cast from datetime import datetime, timezone -from typing_extensions import Literal, Annotated, TypeAliasType +from collections import deque +from typing_extensions import Literal, Annotated, TypedDict, TypeAliasType import pytest import pydantic @@ -9,7 +10,7 @@ from browserbase._utils import PropertyInfo from browserbase._compat import PYDANTIC_V1, parse_obj, model_dump, model_json -from browserbase._models import DISCRIMINATOR_CACHE, BaseModel, construct_type +from browserbase._models import DISCRIMINATOR_CACHE, BaseModel, EagerIterable, construct_type class BasicModel(BaseModel): @@ -961,3 +962,56 @@ def __getattr__(self, attr: str) -> Item: ... assert model.a.prop == 1 assert isinstance(model.a, Item) assert model.other == "foo" + + +# NOTE: Workaround for Pydantic Iterable behavior. +# Iterable fields are replaced with a ValidatorIterator and may be consumed +# during serialization, which can cause subsequent dumps to return empty data. +# See: https://github.com/pydantic/pydantic/issues/9541 +@pytest.mark.parametrize( + "data, expected_validated", + [ + ([1, 2, 3], [1, 2, 3]), + ((1, 2, 3), (1, 2, 3)), + (set([1, 2, 3]), set([1, 2, 3])), + (iter([1, 2, 3]), [1, 2, 3]), + ([], []), + ((x for x in [1, 2, 3]), [1, 2, 3]), + (map(lambda x: x, [1, 2, 3]), [1, 2, 3]), + (frozenset([1, 2, 3]), frozenset([1, 2, 3])), + (deque([1, 2, 3]), deque([1, 2, 3])), + ], + ids=["list", "tuple", "set", "iterator", "empty", "generator", "map", "frozenset", "deque"], +) +@pytest.mark.skipif(PYDANTIC_V1, reason="this is only supported in pydantic v2") +def test_iterable_construction(data: Iterable[int], expected_validated: Iterable[int]) -> None: + class TypeWithIterable(TypedDict): + items: EagerIterable[int] + + class Model(BaseModel): + data: TypeWithIterable + + m = Model.model_validate({"data": {"items": data}}) + assert m.data["items"] == expected_validated + + # Verify repeated dumps don't lose data (the original bug) + assert m.model_dump()["data"]["items"] == list(expected_validated) + assert m.model_dump()["data"]["items"] == list(expected_validated) + + +@pytest.mark.skipif(PYDANTIC_V1, reason="this is only supported in pydantic v2") +def test_iterable_construction_str_falls_back_to_list() -> None: + # str is iterable (over chars), but str(list_of_chars) produces the list's repr + # rather than reconstructing a string from items. We special-case str to fall + # back to list instead of attempting reconstruction. + class TypeWithIterable(TypedDict): + items: EagerIterable[str] + + class Model(BaseModel): + data: TypeWithIterable + + m = Model.model_validate({"data": {"items": "hello"}}) + + # falls back to list of chars rather than calling str(["h", "e", "l", "l", "o"]) + assert m.data["items"] == ["h", "e", "l", "l", "o"] + assert m.model_dump()["data"]["items"] == ["h", "e", "l", "l", "o"] From 52099d9333466c75df5b1a041155e24e4719ef29 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 12 May 2026 22:41:23 +0000 Subject: [PATCH 307/330] codegen metadata --- .stats.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index e5d38fd8..280b1ba8 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 21 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase/browserbase-213a694dcb7548e9da9f79fdc415ae79475f51fed8353bc684b56bef5208e4ff.yml -openapi_spec_hash: 40adcceb64631c781311492ba14e8c8f +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase/browserbase-aa1d52e43545fc6346d085296cbc6102850ebbcc970c9eac5dc38ec3e50fd396.yml +openapi_spec_hash: 8e48a39a55a11b128028b47747aea775 config_hash: cf04ecfb8dad5fbd8b85be25d6e9ec55 From b3a44be87792af2f4369622acf871a8d5d0cd547 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 13 May 2026 02:40:12 +0000 Subject: [PATCH 308/330] ci: pin GitHub Actions to commit SHAs Pin all GitHub Actions referenced in generated workflows (both first-party `actions/*` and third-party) to immutable commit SHAs. Updating pinned actions is now a deliberate codegen-side bump rather than implicit on every workflow run. --- .github/workflows/ci.yml | 8 ++++---- .github/workflows/publish-pypi.yml | 2 +- .github/workflows/release-doctor.yml | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9c4beaa4..0b57e9e3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,7 +21,7 @@ jobs: runs-on: ${{ github.repository == 'stainless-sdks/browserbase-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} if: (github.event_name == 'push' || github.event.pull_request.head.repo.fork) && (github.event_name != 'push' || github.event.head_commit.message != 'codegen metadata') steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Install Rye run: | @@ -46,7 +46,7 @@ jobs: id-token: write runs-on: ${{ github.repository == 'stainless-sdks/browserbase-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Install Rye run: | @@ -67,7 +67,7 @@ jobs: github.repository == 'stainless-sdks/browserbase-python' && !startsWith(github.ref, 'refs/heads/stl/') id: github-oidc - uses: actions/github-script@v8 + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 with: script: core.setOutput('github_token', await core.getIDToken()); @@ -87,7 +87,7 @@ jobs: runs-on: ${{ github.repository == 'stainless-sdks/browserbase-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} if: github.event_name == 'push' || github.event.pull_request.head.repo.fork steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Install Rye run: | diff --git a/.github/workflows/publish-pypi.yml b/.github/workflows/publish-pypi.yml index 7fb6d449..ebe645f8 100644 --- a/.github/workflows/publish-pypi.yml +++ b/.github/workflows/publish-pypi.yml @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Install Rye run: | diff --git a/.github/workflows/release-doctor.yml b/.github/workflows/release-doctor.yml index 5beedb0d..1760864d 100644 --- a/.github/workflows/release-doctor.yml +++ b/.github/workflows/release-doctor.yml @@ -12,7 +12,7 @@ jobs: if: github.repository == 'browserbase/sdk-python' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch' || startsWith(github.head_ref, 'release-please') || github.head_ref == 'next') steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Check release environment run: | From b4d12f28c163848d5b4a7576a0cd5579edebd1f3 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 13 May 2026 16:21:49 +0000 Subject: [PATCH 309/330] feat(api): add replays --- .stats.yml | 6 +- api.md | 13 + .../resources/sessions/__init__.py | 14 + src/browserbase/resources/sessions/replays.py | 266 ++++++++++++++++++ .../resources/sessions/sessions.py | 32 +++ src/browserbase/types/sessions/__init__.py | 1 + .../sessions/replay_retrieve_response.py | 25 ++ tests/api_resources/sessions/test_replays.py | 242 ++++++++++++++++ 8 files changed, 596 insertions(+), 3 deletions(-) create mode 100644 src/browserbase/resources/sessions/replays.py create mode 100644 src/browserbase/types/sessions/replay_retrieve_response.py create mode 100644 tests/api_resources/sessions/test_replays.py diff --git a/.stats.yml b/.stats.yml index 280b1ba8..4e2d03e1 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ -configured_endpoints: 21 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase/browserbase-aa1d52e43545fc6346d085296cbc6102850ebbcc970c9eac5dc38ec3e50fd396.yml +configured_endpoints: 23 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase/browserbase-2118fd938d408dda6ed82d06c48b0785fad91fd54b5397acc3421a49a386c791.yml openapi_spec_hash: 8e48a39a55a11b128028b47747aea775 -config_hash: cf04ecfb8dad5fbd8b85be25d6e9ec55 +config_hash: 40fbac80e24faaa0dc19e93368bcd821 diff --git a/api.md b/api.md index b6066cb8..581574a3 100644 --- a/api.md +++ b/api.md @@ -128,3 +128,16 @@ from browserbase.types.sessions import UploadCreateResponse Methods: - client.sessions.uploads.create(id, \*\*params) -> UploadCreateResponse + +## Replays + +Types: + +```python +from browserbase.types.sessions import ReplayRetrieveResponse +``` + +Methods: + +- client.sessions.replays.retrieve(id) -> ReplayRetrieveResponse +- client.sessions.replays.retrieve_page(page_id, \*, id) -> BinaryAPIResponse diff --git a/src/browserbase/resources/sessions/__init__.py b/src/browserbase/resources/sessions/__init__.py index b3877e12..e66ee0ce 100644 --- a/src/browserbase/resources/sessions/__init__.py +++ b/src/browserbase/resources/sessions/__init__.py @@ -8,6 +8,14 @@ LogsResourceWithStreamingResponse, AsyncLogsResourceWithStreamingResponse, ) +from .replays import ( + ReplaysResource, + AsyncReplaysResource, + ReplaysResourceWithRawResponse, + AsyncReplaysResourceWithRawResponse, + ReplaysResourceWithStreamingResponse, + AsyncReplaysResourceWithStreamingResponse, +) from .uploads import ( UploadsResource, AsyncUploadsResource, @@ -66,6 +74,12 @@ "AsyncUploadsResourceWithRawResponse", "UploadsResourceWithStreamingResponse", "AsyncUploadsResourceWithStreamingResponse", + "ReplaysResource", + "AsyncReplaysResource", + "ReplaysResourceWithRawResponse", + "AsyncReplaysResourceWithRawResponse", + "ReplaysResourceWithStreamingResponse", + "AsyncReplaysResourceWithStreamingResponse", "SessionsResource", "AsyncSessionsResource", "SessionsResourceWithRawResponse", diff --git a/src/browserbase/resources/sessions/replays.py b/src/browserbase/resources/sessions/replays.py new file mode 100644 index 00000000..c9240356 --- /dev/null +++ b/src/browserbase/resources/sessions/replays.py @@ -0,0 +1,266 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import httpx + +from ..._types import Body, Query, Headers, NotGiven, not_given +from ..._utils import path_template +from ..._compat import cached_property +from ..._resource import SyncAPIResource, AsyncAPIResource +from ..._response import ( + BinaryAPIResponse, + AsyncBinaryAPIResponse, + StreamedBinaryAPIResponse, + AsyncStreamedBinaryAPIResponse, + to_raw_response_wrapper, + to_streamed_response_wrapper, + async_to_raw_response_wrapper, + to_custom_raw_response_wrapper, + async_to_streamed_response_wrapper, + to_custom_streamed_response_wrapper, + async_to_custom_raw_response_wrapper, + async_to_custom_streamed_response_wrapper, +) +from ..._base_client import make_request_options +from ...types.sessions.replay_retrieve_response import ReplayRetrieveResponse + +__all__ = ["ReplaysResource", "AsyncReplaysResource"] + + +class ReplaysResource(SyncAPIResource): + @cached_property + def with_raw_response(self) -> ReplaysResourceWithRawResponse: + """ + 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/sdk-python#accessing-raw-response-data-eg-headers + """ + return ReplaysResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> ReplaysResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/browserbase/sdk-python#with_streaming_response + """ + return ReplaysResourceWithStreamingResponse(self) + + def retrieve( + self, + 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, + ) -> ReplayRetrieveResponse: + """ + Returns page metadata for a session replay, including timing information and the + URL of each page's HLS playlist. + + 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 id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return self._get( + path_template("/v1/sessions/{id}/replays", id=id), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=ReplayRetrieveResponse, + ) + + def retrieve_page( + self, + page_id: str, + *, + 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, + ) -> BinaryAPIResponse: + """ + Returns an HLS VOD media playlist (.m3u8) for a specific page of a session + replay. + + 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 id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + if not page_id: + raise ValueError(f"Expected a non-empty value for `page_id` but received {page_id!r}") + extra_headers = {"Accept": "application/vnd.apple.mpegurl", **(extra_headers or {})} + return self._get( + path_template("/v1/sessions/{id}/replays/{page_id}", id=id, page_id=page_id), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=BinaryAPIResponse, + ) + + +class AsyncReplaysResource(AsyncAPIResource): + @cached_property + def with_raw_response(self) -> AsyncReplaysResourceWithRawResponse: + """ + 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/sdk-python#accessing-raw-response-data-eg-headers + """ + return AsyncReplaysResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncReplaysResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/browserbase/sdk-python#with_streaming_response + """ + return AsyncReplaysResourceWithStreamingResponse(self) + + async def retrieve( + self, + 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, + ) -> ReplayRetrieveResponse: + """ + Returns page metadata for a session replay, including timing information and the + URL of each page's HLS playlist. + + 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 id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return await self._get( + path_template("/v1/sessions/{id}/replays", id=id), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=ReplayRetrieveResponse, + ) + + async def retrieve_page( + self, + page_id: str, + *, + 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, + ) -> AsyncBinaryAPIResponse: + """ + Returns an HLS VOD media playlist (.m3u8) for a specific page of a session + replay. + + 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 id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + if not page_id: + raise ValueError(f"Expected a non-empty value for `page_id` but received {page_id!r}") + extra_headers = {"Accept": "application/vnd.apple.mpegurl", **(extra_headers or {})} + return await self._get( + path_template("/v1/sessions/{id}/replays/{page_id}", id=id, page_id=page_id), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=AsyncBinaryAPIResponse, + ) + + +class ReplaysResourceWithRawResponse: + def __init__(self, replays: ReplaysResource) -> None: + self._replays = replays + + self.retrieve = to_raw_response_wrapper( + replays.retrieve, + ) + self.retrieve_page = to_custom_raw_response_wrapper( + replays.retrieve_page, + BinaryAPIResponse, + ) + + +class AsyncReplaysResourceWithRawResponse: + def __init__(self, replays: AsyncReplaysResource) -> None: + self._replays = replays + + self.retrieve = async_to_raw_response_wrapper( + replays.retrieve, + ) + self.retrieve_page = async_to_custom_raw_response_wrapper( + replays.retrieve_page, + AsyncBinaryAPIResponse, + ) + + +class ReplaysResourceWithStreamingResponse: + def __init__(self, replays: ReplaysResource) -> None: + self._replays = replays + + self.retrieve = to_streamed_response_wrapper( + replays.retrieve, + ) + self.retrieve_page = to_custom_streamed_response_wrapper( + replays.retrieve_page, + StreamedBinaryAPIResponse, + ) + + +class AsyncReplaysResourceWithStreamingResponse: + def __init__(self, replays: AsyncReplaysResource) -> None: + self._replays = replays + + self.retrieve = async_to_streamed_response_wrapper( + replays.retrieve, + ) + self.retrieve_page = async_to_custom_streamed_response_wrapper( + replays.retrieve_page, + AsyncStreamedBinaryAPIResponse, + ) diff --git a/src/browserbase/resources/sessions/sessions.py b/src/browserbase/resources/sessions/sessions.py index ce3de98f..a54d7a72 100644 --- a/src/browserbase/resources/sessions/sessions.py +++ b/src/browserbase/resources/sessions/sessions.py @@ -16,6 +16,14 @@ AsyncLogsResourceWithStreamingResponse, ) from ...types import session_list_params, session_create_params, session_update_params +from .replays import ( + ReplaysResource, + AsyncReplaysResource, + ReplaysResourceWithRawResponse, + AsyncReplaysResourceWithRawResponse, + ReplaysResourceWithStreamingResponse, + AsyncReplaysResourceWithStreamingResponse, +) from .uploads import ( UploadsResource, AsyncUploadsResource, @@ -77,6 +85,10 @@ def recording(self) -> RecordingResource: def uploads(self) -> UploadsResource: return UploadsResource(self._client) + @cached_property + def replays(self) -> ReplaysResource: + return ReplaysResource(self._client) + @cached_property def with_raw_response(self) -> SessionsResourceWithRawResponse: """ @@ -349,6 +361,10 @@ def recording(self) -> AsyncRecordingResource: def uploads(self) -> AsyncUploadsResource: return AsyncUploadsResource(self._client) + @cached_property + def replays(self) -> AsyncReplaysResource: + return AsyncReplaysResource(self._client) + @cached_property def with_raw_response(self) -> AsyncSessionsResourceWithRawResponse: """ @@ -640,6 +656,10 @@ def recording(self) -> RecordingResourceWithRawResponse: def uploads(self) -> UploadsResourceWithRawResponse: return UploadsResourceWithRawResponse(self._sessions.uploads) + @cached_property + def replays(self) -> ReplaysResourceWithRawResponse: + return ReplaysResourceWithRawResponse(self._sessions.replays) + class AsyncSessionsResourceWithRawResponse: def __init__(self, sessions: AsyncSessionsResource) -> None: @@ -677,6 +697,10 @@ def recording(self) -> AsyncRecordingResourceWithRawResponse: def uploads(self) -> AsyncUploadsResourceWithRawResponse: return AsyncUploadsResourceWithRawResponse(self._sessions.uploads) + @cached_property + def replays(self) -> AsyncReplaysResourceWithRawResponse: + return AsyncReplaysResourceWithRawResponse(self._sessions.replays) + class SessionsResourceWithStreamingResponse: def __init__(self, sessions: SessionsResource) -> None: @@ -714,6 +738,10 @@ def recording(self) -> RecordingResourceWithStreamingResponse: def uploads(self) -> UploadsResourceWithStreamingResponse: return UploadsResourceWithStreamingResponse(self._sessions.uploads) + @cached_property + def replays(self) -> ReplaysResourceWithStreamingResponse: + return ReplaysResourceWithStreamingResponse(self._sessions.replays) + class AsyncSessionsResourceWithStreamingResponse: def __init__(self, sessions: AsyncSessionsResource) -> None: @@ -750,3 +778,7 @@ def recording(self) -> AsyncRecordingResourceWithStreamingResponse: @cached_property def uploads(self) -> AsyncUploadsResourceWithStreamingResponse: return AsyncUploadsResourceWithStreamingResponse(self._sessions.uploads) + + @cached_property + def replays(self) -> AsyncReplaysResourceWithStreamingResponse: + return AsyncReplaysResourceWithStreamingResponse(self._sessions.replays) diff --git a/src/browserbase/types/sessions/__init__.py b/src/browserbase/types/sessions/__init__.py index 0cef6b19..c7ea4671 100644 --- a/src/browserbase/types/sessions/__init__.py +++ b/src/browserbase/types/sessions/__init__.py @@ -7,4 +7,5 @@ from .session_recording import SessionRecording as SessionRecording from .upload_create_params import UploadCreateParams as UploadCreateParams from .upload_create_response import UploadCreateResponse as UploadCreateResponse +from .replay_retrieve_response import ReplayRetrieveResponse as ReplayRetrieveResponse from .recording_retrieve_response import RecordingRetrieveResponse as RecordingRetrieveResponse diff --git a/src/browserbase/types/sessions/replay_retrieve_response.py b/src/browserbase/types/sessions/replay_retrieve_response.py new file mode 100644 index 00000000..7f0b02c0 --- /dev/null +++ b/src/browserbase/types/sessions/replay_retrieve_response.py @@ -0,0 +1,25 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List + +from pydantic import Field as FieldInfo + +from ..._models import BaseModel + +__all__ = ["ReplayRetrieveResponse", "Page"] + + +class Page(BaseModel): + end_time_ms: float = FieldInfo(alias="endTimeMs") + + page_id: str = FieldInfo(alias="pageId") + + start_time_ms: float = FieldInfo(alias="startTimeMs") + + url: str + + +class ReplayRetrieveResponse(BaseModel): + page_count: int = FieldInfo(alias="pageCount") + + pages: List[Page] diff --git a/tests/api_resources/sessions/test_replays.py b/tests/api_resources/sessions/test_replays.py new file mode 100644 index 00000000..a82c7880 --- /dev/null +++ b/tests/api_resources/sessions/test_replays.py @@ -0,0 +1,242 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import os +from typing import Any, cast + +import httpx +import pytest +from respx import MockRouter + +from browserbase import Browserbase, AsyncBrowserbase +from tests.utils import assert_matches_type +from browserbase._response import ( + BinaryAPIResponse, + AsyncBinaryAPIResponse, + StreamedBinaryAPIResponse, + AsyncStreamedBinaryAPIResponse, +) +from browserbase.types.sessions import ReplayRetrieveResponse + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + + +class TestReplays: + parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) + + @parametrize + def test_method_retrieve(self, client: Browserbase) -> None: + replay = client.sessions.replays.retrieve( + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + assert_matches_type(ReplayRetrieveResponse, replay, path=["response"]) + + @parametrize + def test_raw_response_retrieve(self, client: Browserbase) -> None: + response = client.sessions.replays.with_raw_response.retrieve( + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + replay = response.parse() + assert_matches_type(ReplayRetrieveResponse, replay, path=["response"]) + + @parametrize + def test_streaming_response_retrieve(self, client: Browserbase) -> None: + with client.sessions.replays.with_streaming_response.retrieve( + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + replay = response.parse() + assert_matches_type(ReplayRetrieveResponse, replay, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_retrieve(self, client: Browserbase) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.sessions.replays.with_raw_response.retrieve( + "", + ) + + @parametrize + @pytest.mark.respx(base_url=base_url) + def test_method_retrieve_page(self, client: Browserbase, respx_mock: MockRouter) -> None: + respx_mock.get("/v1/sessions/182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e/replays/090").mock( + return_value=httpx.Response(200, json={"foo": "bar"}) + ) + replay = client.sessions.replays.retrieve_page( + page_id="090", + id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + assert replay.is_closed + assert replay.json() == {"foo": "bar"} + assert cast(Any, replay.is_closed) is True + assert isinstance(replay, BinaryAPIResponse) + + @parametrize + @pytest.mark.respx(base_url=base_url) + def test_raw_response_retrieve_page(self, client: Browserbase, respx_mock: MockRouter) -> None: + respx_mock.get("/v1/sessions/182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e/replays/090").mock( + return_value=httpx.Response(200, json={"foo": "bar"}) + ) + + replay = client.sessions.replays.with_raw_response.retrieve_page( + page_id="090", + id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + + assert replay.is_closed is True + assert replay.http_request.headers.get("X-Stainless-Lang") == "python" + assert replay.json() == {"foo": "bar"} + assert isinstance(replay, BinaryAPIResponse) + + @parametrize + @pytest.mark.respx(base_url=base_url) + def test_streaming_response_retrieve_page(self, client: Browserbase, respx_mock: MockRouter) -> None: + respx_mock.get("/v1/sessions/182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e/replays/090").mock( + return_value=httpx.Response(200, json={"foo": "bar"}) + ) + with client.sessions.replays.with_streaming_response.retrieve_page( + page_id="090", + id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) as replay: + assert not replay.is_closed + assert replay.http_request.headers.get("X-Stainless-Lang") == "python" + + assert replay.json() == {"foo": "bar"} + assert cast(Any, replay.is_closed) is True + assert isinstance(replay, StreamedBinaryAPIResponse) + + assert cast(Any, replay.is_closed) is True + + @parametrize + @pytest.mark.respx(base_url=base_url) + def test_path_params_retrieve_page(self, client: Browserbase) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.sessions.replays.with_raw_response.retrieve_page( + page_id="090", + id="", + ) + + with pytest.raises(ValueError, match=r"Expected a non-empty value for `page_id` but received ''"): + client.sessions.replays.with_raw_response.retrieve_page( + page_id="", + id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + + +class TestAsyncReplays: + parametrize = pytest.mark.parametrize( + "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] + ) + + @parametrize + async def test_method_retrieve(self, async_client: AsyncBrowserbase) -> None: + replay = await async_client.sessions.replays.retrieve( + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + assert_matches_type(ReplayRetrieveResponse, replay, path=["response"]) + + @parametrize + async def test_raw_response_retrieve(self, async_client: AsyncBrowserbase) -> None: + response = await async_client.sessions.replays.with_raw_response.retrieve( + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + replay = await response.parse() + assert_matches_type(ReplayRetrieveResponse, replay, path=["response"]) + + @parametrize + async def test_streaming_response_retrieve(self, async_client: AsyncBrowserbase) -> None: + async with async_client.sessions.replays.with_streaming_response.retrieve( + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + replay = await response.parse() + assert_matches_type(ReplayRetrieveResponse, replay, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_retrieve(self, async_client: AsyncBrowserbase) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.sessions.replays.with_raw_response.retrieve( + "", + ) + + @parametrize + @pytest.mark.respx(base_url=base_url) + async def test_method_retrieve_page(self, async_client: AsyncBrowserbase, respx_mock: MockRouter) -> None: + respx_mock.get("/v1/sessions/182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e/replays/090").mock( + return_value=httpx.Response(200, json={"foo": "bar"}) + ) + replay = await async_client.sessions.replays.retrieve_page( + page_id="090", + id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + assert replay.is_closed + assert await replay.json() == {"foo": "bar"} + assert cast(Any, replay.is_closed) is True + assert isinstance(replay, AsyncBinaryAPIResponse) + + @parametrize + @pytest.mark.respx(base_url=base_url) + async def test_raw_response_retrieve_page(self, async_client: AsyncBrowserbase, respx_mock: MockRouter) -> None: + respx_mock.get("/v1/sessions/182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e/replays/090").mock( + return_value=httpx.Response(200, json={"foo": "bar"}) + ) + + replay = await async_client.sessions.replays.with_raw_response.retrieve_page( + page_id="090", + id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + + assert replay.is_closed is True + assert replay.http_request.headers.get("X-Stainless-Lang") == "python" + assert await replay.json() == {"foo": "bar"} + assert isinstance(replay, AsyncBinaryAPIResponse) + + @parametrize + @pytest.mark.respx(base_url=base_url) + async def test_streaming_response_retrieve_page( + self, async_client: AsyncBrowserbase, respx_mock: MockRouter + ) -> None: + respx_mock.get("/v1/sessions/182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e/replays/090").mock( + return_value=httpx.Response(200, json={"foo": "bar"}) + ) + async with async_client.sessions.replays.with_streaming_response.retrieve_page( + page_id="090", + id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) as replay: + assert not replay.is_closed + assert replay.http_request.headers.get("X-Stainless-Lang") == "python" + + assert await replay.json() == {"foo": "bar"} + assert cast(Any, replay.is_closed) is True + assert isinstance(replay, AsyncStreamedBinaryAPIResponse) + + assert cast(Any, replay.is_closed) is True + + @parametrize + @pytest.mark.respx(base_url=base_url) + async def test_path_params_retrieve_page(self, async_client: AsyncBrowserbase) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.sessions.replays.with_raw_response.retrieve_page( + page_id="090", + id="", + ) + + with pytest.raises(ValueError, match=r"Expected a non-empty value for `page_id` but received ''"): + await async_client.sessions.replays.with_raw_response.retrieve_page( + page_id="", + id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) From 32df91c1c8fd627ebd1a4071c817e96438792cfb Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 13 May 2026 18:54:45 +0000 Subject: [PATCH 310/330] chore(internal): version bump --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/browserbase/_version.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index c523ce19..c3c95522 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "1.8.0" + ".": "1.9.0" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index e3027a02..7274723b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "browserbase" -version = "1.8.0" +version = "1.9.0" description = "The official Python library for the Browserbase API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/browserbase/_version.py b/src/browserbase/_version.py index 10594de4..eaa01ae1 100644 --- a/src/browserbase/_version.py +++ b/src/browserbase/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "browserbase" -__version__ = "1.8.0" # x-release-please-version +__version__ = "1.9.0" # x-release-please-version From a9675e23157c509394303497ea61b803781160ec Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 13 May 2026 20:16:23 +0000 Subject: [PATCH 311/330] feat: Cast replay page start/end times as integer --- .stats.yml | 4 ++-- src/browserbase/types/sessions/replay_retrieve_response.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.stats.yml b/.stats.yml index 4e2d03e1..192797ac 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 23 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase/browserbase-2118fd938d408dda6ed82d06c48b0785fad91fd54b5397acc3421a49a386c791.yml -openapi_spec_hash: 8e48a39a55a11b128028b47747aea775 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase/browserbase-466614a040e7f31307530bd6ba443e714b6303eaa141904e7d32e6641d5ec55f.yml +openapi_spec_hash: 2d06680e7c17847e4fbcac35124d2456 config_hash: 40fbac80e24faaa0dc19e93368bcd821 diff --git a/src/browserbase/types/sessions/replay_retrieve_response.py b/src/browserbase/types/sessions/replay_retrieve_response.py index 7f0b02c0..ec16398a 100644 --- a/src/browserbase/types/sessions/replay_retrieve_response.py +++ b/src/browserbase/types/sessions/replay_retrieve_response.py @@ -10,11 +10,11 @@ class Page(BaseModel): - end_time_ms: float = FieldInfo(alias="endTimeMs") + end_time_ms: int = FieldInfo(alias="endTimeMs") page_id: str = FieldInfo(alias="pageId") - start_time_ms: float = FieldInfo(alias="startTimeMs") + start_time_ms: int = FieldInfo(alias="startTimeMs") url: str From f15fdcb1cea65b969d1710143933f6ffca4bb42d Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 13 May 2026 20:22:52 +0000 Subject: [PATCH 312/330] chore(internal): version bump --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/browserbase/_version.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index c3c95522..eb4e0dba 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "1.9.0" + ".": "1.10.0" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 7274723b..68a1daeb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "browserbase" -version = "1.9.0" +version = "1.10.0" description = "The official Python library for the Browserbase API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/browserbase/_version.py b/src/browserbase/_version.py index eaa01ae1..c811ccc9 100644 --- a/src/browserbase/_version.py +++ b/src/browserbase/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "browserbase" -__version__ = "1.9.0" # x-release-please-version +__version__ = "1.10.0" # x-release-please-version From 71da22bfaa111bd1db1f69511286d7e6b3d39571 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 18 May 2026 21:47:54 +0000 Subject: [PATCH 313/330] feat: [AI-1993] - Surface structured JSON content on /v2/fetch --- .stats.yml | 8 +- api.md | 13 - .../resources/sessions/__init__.py | 14 - src/browserbase/resources/sessions/replays.py | 266 ------------------ .../resources/sessions/sessions.py | 32 --- .../types/fetch_api_create_response.py | 10 +- src/browserbase/types/sessions/__init__.py | 1 - .../sessions/replay_retrieve_response.py | 25 -- tests/api_resources/sessions/test_replays.py | 242 ---------------- 9 files changed, 11 insertions(+), 600 deletions(-) delete mode 100644 src/browserbase/resources/sessions/replays.py delete mode 100644 src/browserbase/types/sessions/replay_retrieve_response.py delete mode 100644 tests/api_resources/sessions/test_replays.py diff --git a/.stats.yml b/.stats.yml index 192797ac..523861e0 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ -configured_endpoints: 23 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase/browserbase-466614a040e7f31307530bd6ba443e714b6303eaa141904e7d32e6641d5ec55f.yml -openapi_spec_hash: 2d06680e7c17847e4fbcac35124d2456 -config_hash: 40fbac80e24faaa0dc19e93368bcd821 +configured_endpoints: 21 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase/browserbase-1821faac6d1422fea15b3ba1f88c0f5f00c43464524e17d3fd1efd1ea148b7c4.yml +openapi_spec_hash: 1e3fba074314f557dc6973cf97ea6a69 +config_hash: cf04ecfb8dad5fbd8b85be25d6e9ec55 diff --git a/api.md b/api.md index 581574a3..b6066cb8 100644 --- a/api.md +++ b/api.md @@ -128,16 +128,3 @@ from browserbase.types.sessions import UploadCreateResponse Methods: - client.sessions.uploads.create(id, \*\*params) -> UploadCreateResponse - -## Replays - -Types: - -```python -from browserbase.types.sessions import ReplayRetrieveResponse -``` - -Methods: - -- client.sessions.replays.retrieve(id) -> ReplayRetrieveResponse -- client.sessions.replays.retrieve_page(page_id, \*, id) -> BinaryAPIResponse diff --git a/src/browserbase/resources/sessions/__init__.py b/src/browserbase/resources/sessions/__init__.py index e66ee0ce..b3877e12 100644 --- a/src/browserbase/resources/sessions/__init__.py +++ b/src/browserbase/resources/sessions/__init__.py @@ -8,14 +8,6 @@ LogsResourceWithStreamingResponse, AsyncLogsResourceWithStreamingResponse, ) -from .replays import ( - ReplaysResource, - AsyncReplaysResource, - ReplaysResourceWithRawResponse, - AsyncReplaysResourceWithRawResponse, - ReplaysResourceWithStreamingResponse, - AsyncReplaysResourceWithStreamingResponse, -) from .uploads import ( UploadsResource, AsyncUploadsResource, @@ -74,12 +66,6 @@ "AsyncUploadsResourceWithRawResponse", "UploadsResourceWithStreamingResponse", "AsyncUploadsResourceWithStreamingResponse", - "ReplaysResource", - "AsyncReplaysResource", - "ReplaysResourceWithRawResponse", - "AsyncReplaysResourceWithRawResponse", - "ReplaysResourceWithStreamingResponse", - "AsyncReplaysResourceWithStreamingResponse", "SessionsResource", "AsyncSessionsResource", "SessionsResourceWithRawResponse", diff --git a/src/browserbase/resources/sessions/replays.py b/src/browserbase/resources/sessions/replays.py deleted file mode 100644 index c9240356..00000000 --- a/src/browserbase/resources/sessions/replays.py +++ /dev/null @@ -1,266 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -import httpx - -from ..._types import Body, Query, Headers, NotGiven, not_given -from ..._utils import path_template -from ..._compat import cached_property -from ..._resource import SyncAPIResource, AsyncAPIResource -from ..._response import ( - BinaryAPIResponse, - AsyncBinaryAPIResponse, - StreamedBinaryAPIResponse, - AsyncStreamedBinaryAPIResponse, - to_raw_response_wrapper, - to_streamed_response_wrapper, - async_to_raw_response_wrapper, - to_custom_raw_response_wrapper, - async_to_streamed_response_wrapper, - to_custom_streamed_response_wrapper, - async_to_custom_raw_response_wrapper, - async_to_custom_streamed_response_wrapper, -) -from ..._base_client import make_request_options -from ...types.sessions.replay_retrieve_response import ReplayRetrieveResponse - -__all__ = ["ReplaysResource", "AsyncReplaysResource"] - - -class ReplaysResource(SyncAPIResource): - @cached_property - def with_raw_response(self) -> ReplaysResourceWithRawResponse: - """ - 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/sdk-python#accessing-raw-response-data-eg-headers - """ - return ReplaysResourceWithRawResponse(self) - - @cached_property - def with_streaming_response(self) -> ReplaysResourceWithStreamingResponse: - """ - An alternative to `.with_raw_response` that doesn't eagerly read the response body. - - For more information, see https://www.github.com/browserbase/sdk-python#with_streaming_response - """ - return ReplaysResourceWithStreamingResponse(self) - - def retrieve( - self, - 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, - ) -> ReplayRetrieveResponse: - """ - Returns page metadata for a session replay, including timing information and the - URL of each page's HLS playlist. - - 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 id: - raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") - return self._get( - path_template("/v1/sessions/{id}/replays", id=id), - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=ReplayRetrieveResponse, - ) - - def retrieve_page( - self, - page_id: str, - *, - 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, - ) -> BinaryAPIResponse: - """ - Returns an HLS VOD media playlist (.m3u8) for a specific page of a session - replay. - - 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 id: - raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") - if not page_id: - raise ValueError(f"Expected a non-empty value for `page_id` but received {page_id!r}") - extra_headers = {"Accept": "application/vnd.apple.mpegurl", **(extra_headers or {})} - return self._get( - path_template("/v1/sessions/{id}/replays/{page_id}", id=id, page_id=page_id), - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=BinaryAPIResponse, - ) - - -class AsyncReplaysResource(AsyncAPIResource): - @cached_property - def with_raw_response(self) -> AsyncReplaysResourceWithRawResponse: - """ - 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/sdk-python#accessing-raw-response-data-eg-headers - """ - return AsyncReplaysResourceWithRawResponse(self) - - @cached_property - def with_streaming_response(self) -> AsyncReplaysResourceWithStreamingResponse: - """ - An alternative to `.with_raw_response` that doesn't eagerly read the response body. - - For more information, see https://www.github.com/browserbase/sdk-python#with_streaming_response - """ - return AsyncReplaysResourceWithStreamingResponse(self) - - async def retrieve( - self, - 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, - ) -> ReplayRetrieveResponse: - """ - Returns page metadata for a session replay, including timing information and the - URL of each page's HLS playlist. - - 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 id: - raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") - return await self._get( - path_template("/v1/sessions/{id}/replays", id=id), - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=ReplayRetrieveResponse, - ) - - async def retrieve_page( - self, - page_id: str, - *, - 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, - ) -> AsyncBinaryAPIResponse: - """ - Returns an HLS VOD media playlist (.m3u8) for a specific page of a session - replay. - - 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 id: - raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") - if not page_id: - raise ValueError(f"Expected a non-empty value for `page_id` but received {page_id!r}") - extra_headers = {"Accept": "application/vnd.apple.mpegurl", **(extra_headers or {})} - return await self._get( - path_template("/v1/sessions/{id}/replays/{page_id}", id=id, page_id=page_id), - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=AsyncBinaryAPIResponse, - ) - - -class ReplaysResourceWithRawResponse: - def __init__(self, replays: ReplaysResource) -> None: - self._replays = replays - - self.retrieve = to_raw_response_wrapper( - replays.retrieve, - ) - self.retrieve_page = to_custom_raw_response_wrapper( - replays.retrieve_page, - BinaryAPIResponse, - ) - - -class AsyncReplaysResourceWithRawResponse: - def __init__(self, replays: AsyncReplaysResource) -> None: - self._replays = replays - - self.retrieve = async_to_raw_response_wrapper( - replays.retrieve, - ) - self.retrieve_page = async_to_custom_raw_response_wrapper( - replays.retrieve_page, - AsyncBinaryAPIResponse, - ) - - -class ReplaysResourceWithStreamingResponse: - def __init__(self, replays: ReplaysResource) -> None: - self._replays = replays - - self.retrieve = to_streamed_response_wrapper( - replays.retrieve, - ) - self.retrieve_page = to_custom_streamed_response_wrapper( - replays.retrieve_page, - StreamedBinaryAPIResponse, - ) - - -class AsyncReplaysResourceWithStreamingResponse: - def __init__(self, replays: AsyncReplaysResource) -> None: - self._replays = replays - - self.retrieve = async_to_streamed_response_wrapper( - replays.retrieve, - ) - self.retrieve_page = async_to_custom_streamed_response_wrapper( - replays.retrieve_page, - AsyncStreamedBinaryAPIResponse, - ) diff --git a/src/browserbase/resources/sessions/sessions.py b/src/browserbase/resources/sessions/sessions.py index a54d7a72..ce3de98f 100644 --- a/src/browserbase/resources/sessions/sessions.py +++ b/src/browserbase/resources/sessions/sessions.py @@ -16,14 +16,6 @@ AsyncLogsResourceWithStreamingResponse, ) from ...types import session_list_params, session_create_params, session_update_params -from .replays import ( - ReplaysResource, - AsyncReplaysResource, - ReplaysResourceWithRawResponse, - AsyncReplaysResourceWithRawResponse, - ReplaysResourceWithStreamingResponse, - AsyncReplaysResourceWithStreamingResponse, -) from .uploads import ( UploadsResource, AsyncUploadsResource, @@ -85,10 +77,6 @@ def recording(self) -> RecordingResource: def uploads(self) -> UploadsResource: return UploadsResource(self._client) - @cached_property - def replays(self) -> ReplaysResource: - return ReplaysResource(self._client) - @cached_property def with_raw_response(self) -> SessionsResourceWithRawResponse: """ @@ -361,10 +349,6 @@ def recording(self) -> AsyncRecordingResource: def uploads(self) -> AsyncUploadsResource: return AsyncUploadsResource(self._client) - @cached_property - def replays(self) -> AsyncReplaysResource: - return AsyncReplaysResource(self._client) - @cached_property def with_raw_response(self) -> AsyncSessionsResourceWithRawResponse: """ @@ -656,10 +640,6 @@ def recording(self) -> RecordingResourceWithRawResponse: def uploads(self) -> UploadsResourceWithRawResponse: return UploadsResourceWithRawResponse(self._sessions.uploads) - @cached_property - def replays(self) -> ReplaysResourceWithRawResponse: - return ReplaysResourceWithRawResponse(self._sessions.replays) - class AsyncSessionsResourceWithRawResponse: def __init__(self, sessions: AsyncSessionsResource) -> None: @@ -697,10 +677,6 @@ def recording(self) -> AsyncRecordingResourceWithRawResponse: def uploads(self) -> AsyncUploadsResourceWithRawResponse: return AsyncUploadsResourceWithRawResponse(self._sessions.uploads) - @cached_property - def replays(self) -> AsyncReplaysResourceWithRawResponse: - return AsyncReplaysResourceWithRawResponse(self._sessions.replays) - class SessionsResourceWithStreamingResponse: def __init__(self, sessions: SessionsResource) -> None: @@ -738,10 +714,6 @@ def recording(self) -> RecordingResourceWithStreamingResponse: def uploads(self) -> UploadsResourceWithStreamingResponse: return UploadsResourceWithStreamingResponse(self._sessions.uploads) - @cached_property - def replays(self) -> ReplaysResourceWithStreamingResponse: - return ReplaysResourceWithStreamingResponse(self._sessions.replays) - class AsyncSessionsResourceWithStreamingResponse: def __init__(self, sessions: AsyncSessionsResource) -> None: @@ -778,7 +750,3 @@ def recording(self) -> AsyncRecordingResourceWithStreamingResponse: @cached_property def uploads(self) -> AsyncUploadsResourceWithStreamingResponse: return AsyncUploadsResourceWithStreamingResponse(self._sessions.uploads) - - @cached_property - def replays(self) -> AsyncReplaysResourceWithStreamingResponse: - return AsyncReplaysResourceWithStreamingResponse(self._sessions.replays) diff --git a/src/browserbase/types/fetch_api_create_response.py b/src/browserbase/types/fetch_api_create_response.py index f97f5635..6a378000 100644 --- a/src/browserbase/types/fetch_api_create_response.py +++ b/src/browserbase/types/fetch_api_create_response.py @@ -1,6 +1,6 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -from typing import Dict +from typing import Dict, Union from pydantic import Field as FieldInfo @@ -13,8 +13,12 @@ class FetchAPICreateResponse(BaseModel): id: str """Unique identifier for the fetch request""" - content: str - """The response body content""" + content: Union[str, Dict[str, object]] + """The response body content. + + A string for `raw` and `markdown` formats; a structured object for `json` format + (the schema-extracted result). + """ content_type: str = FieldInfo(alias="contentType") """The MIME type of the response""" diff --git a/src/browserbase/types/sessions/__init__.py b/src/browserbase/types/sessions/__init__.py index c7ea4671..0cef6b19 100644 --- a/src/browserbase/types/sessions/__init__.py +++ b/src/browserbase/types/sessions/__init__.py @@ -7,5 +7,4 @@ from .session_recording import SessionRecording as SessionRecording from .upload_create_params import UploadCreateParams as UploadCreateParams from .upload_create_response import UploadCreateResponse as UploadCreateResponse -from .replay_retrieve_response import ReplayRetrieveResponse as ReplayRetrieveResponse from .recording_retrieve_response import RecordingRetrieveResponse as RecordingRetrieveResponse diff --git a/src/browserbase/types/sessions/replay_retrieve_response.py b/src/browserbase/types/sessions/replay_retrieve_response.py deleted file mode 100644 index ec16398a..00000000 --- a/src/browserbase/types/sessions/replay_retrieve_response.py +++ /dev/null @@ -1,25 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from typing import List - -from pydantic import Field as FieldInfo - -from ..._models import BaseModel - -__all__ = ["ReplayRetrieveResponse", "Page"] - - -class Page(BaseModel): - end_time_ms: int = FieldInfo(alias="endTimeMs") - - page_id: str = FieldInfo(alias="pageId") - - start_time_ms: int = FieldInfo(alias="startTimeMs") - - url: str - - -class ReplayRetrieveResponse(BaseModel): - page_count: int = FieldInfo(alias="pageCount") - - pages: List[Page] diff --git a/tests/api_resources/sessions/test_replays.py b/tests/api_resources/sessions/test_replays.py deleted file mode 100644 index a82c7880..00000000 --- a/tests/api_resources/sessions/test_replays.py +++ /dev/null @@ -1,242 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -import os -from typing import Any, cast - -import httpx -import pytest -from respx import MockRouter - -from browserbase import Browserbase, AsyncBrowserbase -from tests.utils import assert_matches_type -from browserbase._response import ( - BinaryAPIResponse, - AsyncBinaryAPIResponse, - StreamedBinaryAPIResponse, - AsyncStreamedBinaryAPIResponse, -) -from browserbase.types.sessions import ReplayRetrieveResponse - -base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") - - -class TestReplays: - parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) - - @parametrize - def test_method_retrieve(self, client: Browserbase) -> None: - replay = client.sessions.replays.retrieve( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) - assert_matches_type(ReplayRetrieveResponse, replay, path=["response"]) - - @parametrize - def test_raw_response_retrieve(self, client: Browserbase) -> None: - response = client.sessions.replays.with_raw_response.retrieve( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - replay = response.parse() - assert_matches_type(ReplayRetrieveResponse, replay, path=["response"]) - - @parametrize - def test_streaming_response_retrieve(self, client: Browserbase) -> None: - with client.sessions.replays.with_streaming_response.retrieve( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - replay = response.parse() - assert_matches_type(ReplayRetrieveResponse, replay, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @parametrize - def test_path_params_retrieve(self, client: Browserbase) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): - client.sessions.replays.with_raw_response.retrieve( - "", - ) - - @parametrize - @pytest.mark.respx(base_url=base_url) - def test_method_retrieve_page(self, client: Browserbase, respx_mock: MockRouter) -> None: - respx_mock.get("/v1/sessions/182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e/replays/090").mock( - return_value=httpx.Response(200, json={"foo": "bar"}) - ) - replay = client.sessions.replays.retrieve_page( - page_id="090", - id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) - assert replay.is_closed - assert replay.json() == {"foo": "bar"} - assert cast(Any, replay.is_closed) is True - assert isinstance(replay, BinaryAPIResponse) - - @parametrize - @pytest.mark.respx(base_url=base_url) - def test_raw_response_retrieve_page(self, client: Browserbase, respx_mock: MockRouter) -> None: - respx_mock.get("/v1/sessions/182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e/replays/090").mock( - return_value=httpx.Response(200, json={"foo": "bar"}) - ) - - replay = client.sessions.replays.with_raw_response.retrieve_page( - page_id="090", - id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) - - assert replay.is_closed is True - assert replay.http_request.headers.get("X-Stainless-Lang") == "python" - assert replay.json() == {"foo": "bar"} - assert isinstance(replay, BinaryAPIResponse) - - @parametrize - @pytest.mark.respx(base_url=base_url) - def test_streaming_response_retrieve_page(self, client: Browserbase, respx_mock: MockRouter) -> None: - respx_mock.get("/v1/sessions/182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e/replays/090").mock( - return_value=httpx.Response(200, json={"foo": "bar"}) - ) - with client.sessions.replays.with_streaming_response.retrieve_page( - page_id="090", - id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) as replay: - assert not replay.is_closed - assert replay.http_request.headers.get("X-Stainless-Lang") == "python" - - assert replay.json() == {"foo": "bar"} - assert cast(Any, replay.is_closed) is True - assert isinstance(replay, StreamedBinaryAPIResponse) - - assert cast(Any, replay.is_closed) is True - - @parametrize - @pytest.mark.respx(base_url=base_url) - def test_path_params_retrieve_page(self, client: Browserbase) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): - client.sessions.replays.with_raw_response.retrieve_page( - page_id="090", - id="", - ) - - with pytest.raises(ValueError, match=r"Expected a non-empty value for `page_id` but received ''"): - client.sessions.replays.with_raw_response.retrieve_page( - page_id="", - id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) - - -class TestAsyncReplays: - parametrize = pytest.mark.parametrize( - "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] - ) - - @parametrize - async def test_method_retrieve(self, async_client: AsyncBrowserbase) -> None: - replay = await async_client.sessions.replays.retrieve( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) - assert_matches_type(ReplayRetrieveResponse, replay, path=["response"]) - - @parametrize - async def test_raw_response_retrieve(self, async_client: AsyncBrowserbase) -> None: - response = await async_client.sessions.replays.with_raw_response.retrieve( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - replay = await response.parse() - assert_matches_type(ReplayRetrieveResponse, replay, path=["response"]) - - @parametrize - async def test_streaming_response_retrieve(self, async_client: AsyncBrowserbase) -> None: - async with async_client.sessions.replays.with_streaming_response.retrieve( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - replay = await response.parse() - assert_matches_type(ReplayRetrieveResponse, replay, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @parametrize - async def test_path_params_retrieve(self, async_client: AsyncBrowserbase) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): - await async_client.sessions.replays.with_raw_response.retrieve( - "", - ) - - @parametrize - @pytest.mark.respx(base_url=base_url) - async def test_method_retrieve_page(self, async_client: AsyncBrowserbase, respx_mock: MockRouter) -> None: - respx_mock.get("/v1/sessions/182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e/replays/090").mock( - return_value=httpx.Response(200, json={"foo": "bar"}) - ) - replay = await async_client.sessions.replays.retrieve_page( - page_id="090", - id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) - assert replay.is_closed - assert await replay.json() == {"foo": "bar"} - assert cast(Any, replay.is_closed) is True - assert isinstance(replay, AsyncBinaryAPIResponse) - - @parametrize - @pytest.mark.respx(base_url=base_url) - async def test_raw_response_retrieve_page(self, async_client: AsyncBrowserbase, respx_mock: MockRouter) -> None: - respx_mock.get("/v1/sessions/182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e/replays/090").mock( - return_value=httpx.Response(200, json={"foo": "bar"}) - ) - - replay = await async_client.sessions.replays.with_raw_response.retrieve_page( - page_id="090", - id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) - - assert replay.is_closed is True - assert replay.http_request.headers.get("X-Stainless-Lang") == "python" - assert await replay.json() == {"foo": "bar"} - assert isinstance(replay, AsyncBinaryAPIResponse) - - @parametrize - @pytest.mark.respx(base_url=base_url) - async def test_streaming_response_retrieve_page( - self, async_client: AsyncBrowserbase, respx_mock: MockRouter - ) -> None: - respx_mock.get("/v1/sessions/182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e/replays/090").mock( - return_value=httpx.Response(200, json={"foo": "bar"}) - ) - async with async_client.sessions.replays.with_streaming_response.retrieve_page( - page_id="090", - id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) as replay: - assert not replay.is_closed - assert replay.http_request.headers.get("X-Stainless-Lang") == "python" - - assert await replay.json() == {"foo": "bar"} - assert cast(Any, replay.is_closed) is True - assert isinstance(replay, AsyncStreamedBinaryAPIResponse) - - assert cast(Any, replay.is_closed) is True - - @parametrize - @pytest.mark.respx(base_url=base_url) - async def test_path_params_retrieve_page(self, async_client: AsyncBrowserbase) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): - await async_client.sessions.replays.with_raw_response.retrieve_page( - page_id="090", - id="", - ) - - with pytest.raises(ValueError, match=r"Expected a non-empty value for `page_id` but received ''"): - await async_client.sessions.replays.with_raw_response.retrieve_page( - page_id="", - id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) From 624a59ec9ff83df091552a007a13e054b6a6b95d Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 19 May 2026 20:54:49 +0000 Subject: [PATCH 314/330] feat: [AI-1972] - Move fetch v2 handler into /v1/fetch --- .stats.yml | 4 +-- src/browserbase/resources/fetch_api.py | 25 +++++++++++++++++++ .../types/fetch_api_create_params.py | 16 +++++++++++- tests/api_resources/test_fetch_api.py | 4 +++ 4 files changed, 46 insertions(+), 3 deletions(-) diff --git a/.stats.yml b/.stats.yml index 523861e0..85445503 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 21 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase/browserbase-1821faac6d1422fea15b3ba1f88c0f5f00c43464524e17d3fd1efd1ea148b7c4.yml -openapi_spec_hash: 1e3fba074314f557dc6973cf97ea6a69 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase/browserbase-3032c74460ad9c1c4b0a1b2d9107c556f292974a8ce12d525660a9cf31f10bc1.yml +openapi_spec_hash: 8ac1c673ce4e72b88bbbf30100b95e8f config_hash: cf04ecfb8dad5fbd8b85be25d6e9ec55 diff --git a/src/browserbase/resources/fetch_api.py b/src/browserbase/resources/fetch_api.py index dc016722..fc23ac33 100644 --- a/src/browserbase/resources/fetch_api.py +++ b/src/browserbase/resources/fetch_api.py @@ -2,6 +2,9 @@ from __future__ import annotations +from typing import Dict +from typing_extensions import Literal + import httpx from ..types import fetch_api_create_params @@ -47,7 +50,9 @@ def create( url: str, allow_insecure_ssl: bool | Omit = omit, allow_redirects: bool | Omit = omit, + format: Literal["raw", "json", "markdown"] | Omit = omit, proxies: bool | Omit = omit, + schema: Dict[str, 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, @@ -65,8 +70,15 @@ def create( allow_redirects: Whether to follow HTTP redirects + format: Output format for the response content. `raw` (default) returns the response + body unchanged; `json` returns structured data (requires `schema`); `markdown` + returns the page as markdown. + proxies: Whether to enable proxy support for the request + schema: JSON Schema describing the desired structure of the response. Only used when + `format` is `json`. + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -82,7 +94,9 @@ def create( "url": url, "allow_insecure_ssl": allow_insecure_ssl, "allow_redirects": allow_redirects, + "format": format, "proxies": proxies, + "schema": schema, }, fetch_api_create_params.FetchAPICreateParams, ), @@ -119,7 +133,9 @@ async def create( url: str, allow_insecure_ssl: bool | Omit = omit, allow_redirects: bool | Omit = omit, + format: Literal["raw", "json", "markdown"] | Omit = omit, proxies: bool | Omit = omit, + schema: Dict[str, 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, @@ -137,8 +153,15 @@ async def create( allow_redirects: Whether to follow HTTP redirects + format: Output format for the response content. `raw` (default) returns the response + body unchanged; `json` returns structured data (requires `schema`); `markdown` + returns the page as markdown. + proxies: Whether to enable proxy support for the request + schema: JSON Schema describing the desired structure of the response. Only used when + `format` is `json`. + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -154,7 +177,9 @@ async def create( "url": url, "allow_insecure_ssl": allow_insecure_ssl, "allow_redirects": allow_redirects, + "format": format, "proxies": proxies, + "schema": schema, }, fetch_api_create_params.FetchAPICreateParams, ), diff --git a/src/browserbase/types/fetch_api_create_params.py b/src/browserbase/types/fetch_api_create_params.py index 84a8a052..f83096f7 100644 --- a/src/browserbase/types/fetch_api_create_params.py +++ b/src/browserbase/types/fetch_api_create_params.py @@ -2,7 +2,8 @@ from __future__ import annotations -from typing_extensions import Required, Annotated, TypedDict +from typing import Dict +from typing_extensions import Literal, Required, Annotated, TypedDict from .._utils import PropertyInfo @@ -19,5 +20,18 @@ class FetchAPICreateParams(TypedDict, total=False): allow_redirects: Annotated[bool, PropertyInfo(alias="allowRedirects")] """Whether to follow HTTP redirects""" + format: Literal["raw", "json", "markdown"] + """Output format for the response content. + + `raw` (default) returns the response body unchanged; `json` returns structured + data (requires `schema`); `markdown` returns the page as markdown. + """ + proxies: bool """Whether to enable proxy support for the request""" + + schema: Dict[str, object] + """JSON Schema describing the desired structure of the response. + + Only used when `format` is `json`. + """ diff --git a/tests/api_resources/test_fetch_api.py b/tests/api_resources/test_fetch_api.py index b9a0455b..def3304d 100644 --- a/tests/api_resources/test_fetch_api.py +++ b/tests/api_resources/test_fetch_api.py @@ -30,7 +30,9 @@ def test_method_create_with_all_params(self, client: Browserbase) -> None: url="https://example.com", allow_insecure_ssl=True, allow_redirects=True, + format="raw", proxies=True, + schema={"foo": "bar"}, ) assert_matches_type(FetchAPICreateResponse, fetch_api, path=["response"]) @@ -77,7 +79,9 @@ async def test_method_create_with_all_params(self, async_client: AsyncBrowserbas url="https://example.com", allow_insecure_ssl=True, allow_redirects=True, + format="raw", proxies=True, + schema={"foo": "bar"}, ) assert_matches_type(FetchAPICreateResponse, fetch_api, path=["response"]) From d58173156dc1b4a1c23a77e2016f3635a99727b2 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 20 May 2026 10:41:08 +0000 Subject: [PATCH 315/330] feat: [AI-1748][apps/api] Obtain custom certificates in API during session reservation --- .stats.yml | 4 +-- src/browserbase/resources/fetch_api.py | 25 ------------------- .../types/fetch_api_create_params.py | 16 +----------- .../types/fetch_api_create_response.py | 10 +++----- tests/api_resources/test_fetch_api.py | 4 --- 5 files changed, 6 insertions(+), 53 deletions(-) diff --git a/.stats.yml b/.stats.yml index 85445503..c2ae426f 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 21 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase/browserbase-3032c74460ad9c1c4b0a1b2d9107c556f292974a8ce12d525660a9cf31f10bc1.yml -openapi_spec_hash: 8ac1c673ce4e72b88bbbf30100b95e8f +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase/browserbase-090302cbdfdd8758ae5907f93842b16040a66772578d0327349135378e0bce40.yml +openapi_spec_hash: cb19458f0642bea3cf6eaf91113f9fda config_hash: cf04ecfb8dad5fbd8b85be25d6e9ec55 diff --git a/src/browserbase/resources/fetch_api.py b/src/browserbase/resources/fetch_api.py index fc23ac33..dc016722 100644 --- a/src/browserbase/resources/fetch_api.py +++ b/src/browserbase/resources/fetch_api.py @@ -2,9 +2,6 @@ from __future__ import annotations -from typing import Dict -from typing_extensions import Literal - import httpx from ..types import fetch_api_create_params @@ -50,9 +47,7 @@ def create( url: str, allow_insecure_ssl: bool | Omit = omit, allow_redirects: bool | Omit = omit, - format: Literal["raw", "json", "markdown"] | Omit = omit, proxies: bool | Omit = omit, - schema: Dict[str, 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, @@ -70,15 +65,8 @@ def create( allow_redirects: Whether to follow HTTP redirects - format: Output format for the response content. `raw` (default) returns the response - body unchanged; `json` returns structured data (requires `schema`); `markdown` - returns the page as markdown. - proxies: Whether to enable proxy support for the request - schema: JSON Schema describing the desired structure of the response. Only used when - `format` is `json`. - extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -94,9 +82,7 @@ def create( "url": url, "allow_insecure_ssl": allow_insecure_ssl, "allow_redirects": allow_redirects, - "format": format, "proxies": proxies, - "schema": schema, }, fetch_api_create_params.FetchAPICreateParams, ), @@ -133,9 +119,7 @@ async def create( url: str, allow_insecure_ssl: bool | Omit = omit, allow_redirects: bool | Omit = omit, - format: Literal["raw", "json", "markdown"] | Omit = omit, proxies: bool | Omit = omit, - schema: Dict[str, 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, @@ -153,15 +137,8 @@ async def create( allow_redirects: Whether to follow HTTP redirects - format: Output format for the response content. `raw` (default) returns the response - body unchanged; `json` returns structured data (requires `schema`); `markdown` - returns the page as markdown. - proxies: Whether to enable proxy support for the request - schema: JSON Schema describing the desired structure of the response. Only used when - `format` is `json`. - extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -177,9 +154,7 @@ async def create( "url": url, "allow_insecure_ssl": allow_insecure_ssl, "allow_redirects": allow_redirects, - "format": format, "proxies": proxies, - "schema": schema, }, fetch_api_create_params.FetchAPICreateParams, ), diff --git a/src/browserbase/types/fetch_api_create_params.py b/src/browserbase/types/fetch_api_create_params.py index f83096f7..84a8a052 100644 --- a/src/browserbase/types/fetch_api_create_params.py +++ b/src/browserbase/types/fetch_api_create_params.py @@ -2,8 +2,7 @@ from __future__ import annotations -from typing import Dict -from typing_extensions import Literal, Required, Annotated, TypedDict +from typing_extensions import Required, Annotated, TypedDict from .._utils import PropertyInfo @@ -20,18 +19,5 @@ class FetchAPICreateParams(TypedDict, total=False): allow_redirects: Annotated[bool, PropertyInfo(alias="allowRedirects")] """Whether to follow HTTP redirects""" - format: Literal["raw", "json", "markdown"] - """Output format for the response content. - - `raw` (default) returns the response body unchanged; `json` returns structured - data (requires `schema`); `markdown` returns the page as markdown. - """ - proxies: bool """Whether to enable proxy support for the request""" - - schema: Dict[str, object] - """JSON Schema describing the desired structure of the response. - - Only used when `format` is `json`. - """ diff --git a/src/browserbase/types/fetch_api_create_response.py b/src/browserbase/types/fetch_api_create_response.py index 6a378000..f97f5635 100644 --- a/src/browserbase/types/fetch_api_create_response.py +++ b/src/browserbase/types/fetch_api_create_response.py @@ -1,6 +1,6 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -from typing import Dict, Union +from typing import Dict from pydantic import Field as FieldInfo @@ -13,12 +13,8 @@ class FetchAPICreateResponse(BaseModel): id: str """Unique identifier for the fetch request""" - content: Union[str, Dict[str, object]] - """The response body content. - - A string for `raw` and `markdown` formats; a structured object for `json` format - (the schema-extracted result). - """ + content: str + """The response body content""" content_type: str = FieldInfo(alias="contentType") """The MIME type of the response""" diff --git a/tests/api_resources/test_fetch_api.py b/tests/api_resources/test_fetch_api.py index def3304d..b9a0455b 100644 --- a/tests/api_resources/test_fetch_api.py +++ b/tests/api_resources/test_fetch_api.py @@ -30,9 +30,7 @@ def test_method_create_with_all_params(self, client: Browserbase) -> None: url="https://example.com", allow_insecure_ssl=True, allow_redirects=True, - format="raw", proxies=True, - schema={"foo": "bar"}, ) assert_matches_type(FetchAPICreateResponse, fetch_api, path=["response"]) @@ -79,9 +77,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncBrowserbas url="https://example.com", allow_insecure_ssl=True, allow_redirects=True, - format="raw", proxies=True, - schema={"foo": "bar"}, ) assert_matches_type(FetchAPICreateResponse, fetch_api, path=["response"]) From 0aae36bdb50250ca5e61d076a6213042e3f1adb0 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 20 May 2026 20:35:30 +0000 Subject: [PATCH 316/330] feat(api): manual updates --- .stats.yml | 4 +-- src/browserbase/resources/fetch_api.py | 25 +++++++++++++++++++ .../types/fetch_api_create_params.py | 16 +++++++++++- .../types/fetch_api_create_response.py | 10 +++++--- tests/api_resources/test_fetch_api.py | 4 +++ 5 files changed, 53 insertions(+), 6 deletions(-) diff --git a/.stats.yml b/.stats.yml index c2ae426f..85445503 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 21 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase/browserbase-090302cbdfdd8758ae5907f93842b16040a66772578d0327349135378e0bce40.yml -openapi_spec_hash: cb19458f0642bea3cf6eaf91113f9fda +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase/browserbase-3032c74460ad9c1c4b0a1b2d9107c556f292974a8ce12d525660a9cf31f10bc1.yml +openapi_spec_hash: 8ac1c673ce4e72b88bbbf30100b95e8f config_hash: cf04ecfb8dad5fbd8b85be25d6e9ec55 diff --git a/src/browserbase/resources/fetch_api.py b/src/browserbase/resources/fetch_api.py index dc016722..fc23ac33 100644 --- a/src/browserbase/resources/fetch_api.py +++ b/src/browserbase/resources/fetch_api.py @@ -2,6 +2,9 @@ from __future__ import annotations +from typing import Dict +from typing_extensions import Literal + import httpx from ..types import fetch_api_create_params @@ -47,7 +50,9 @@ def create( url: str, allow_insecure_ssl: bool | Omit = omit, allow_redirects: bool | Omit = omit, + format: Literal["raw", "json", "markdown"] | Omit = omit, proxies: bool | Omit = omit, + schema: Dict[str, 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, @@ -65,8 +70,15 @@ def create( allow_redirects: Whether to follow HTTP redirects + format: Output format for the response content. `raw` (default) returns the response + body unchanged; `json` returns structured data (requires `schema`); `markdown` + returns the page as markdown. + proxies: Whether to enable proxy support for the request + schema: JSON Schema describing the desired structure of the response. Only used when + `format` is `json`. + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -82,7 +94,9 @@ def create( "url": url, "allow_insecure_ssl": allow_insecure_ssl, "allow_redirects": allow_redirects, + "format": format, "proxies": proxies, + "schema": schema, }, fetch_api_create_params.FetchAPICreateParams, ), @@ -119,7 +133,9 @@ async def create( url: str, allow_insecure_ssl: bool | Omit = omit, allow_redirects: bool | Omit = omit, + format: Literal["raw", "json", "markdown"] | Omit = omit, proxies: bool | Omit = omit, + schema: Dict[str, 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, @@ -137,8 +153,15 @@ async def create( allow_redirects: Whether to follow HTTP redirects + format: Output format for the response content. `raw` (default) returns the response + body unchanged; `json` returns structured data (requires `schema`); `markdown` + returns the page as markdown. + proxies: Whether to enable proxy support for the request + schema: JSON Schema describing the desired structure of the response. Only used when + `format` is `json`. + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -154,7 +177,9 @@ async def create( "url": url, "allow_insecure_ssl": allow_insecure_ssl, "allow_redirects": allow_redirects, + "format": format, "proxies": proxies, + "schema": schema, }, fetch_api_create_params.FetchAPICreateParams, ), diff --git a/src/browserbase/types/fetch_api_create_params.py b/src/browserbase/types/fetch_api_create_params.py index 84a8a052..f83096f7 100644 --- a/src/browserbase/types/fetch_api_create_params.py +++ b/src/browserbase/types/fetch_api_create_params.py @@ -2,7 +2,8 @@ from __future__ import annotations -from typing_extensions import Required, Annotated, TypedDict +from typing import Dict +from typing_extensions import Literal, Required, Annotated, TypedDict from .._utils import PropertyInfo @@ -19,5 +20,18 @@ class FetchAPICreateParams(TypedDict, total=False): allow_redirects: Annotated[bool, PropertyInfo(alias="allowRedirects")] """Whether to follow HTTP redirects""" + format: Literal["raw", "json", "markdown"] + """Output format for the response content. + + `raw` (default) returns the response body unchanged; `json` returns structured + data (requires `schema`); `markdown` returns the page as markdown. + """ + proxies: bool """Whether to enable proxy support for the request""" + + schema: Dict[str, object] + """JSON Schema describing the desired structure of the response. + + Only used when `format` is `json`. + """ diff --git a/src/browserbase/types/fetch_api_create_response.py b/src/browserbase/types/fetch_api_create_response.py index f97f5635..6a378000 100644 --- a/src/browserbase/types/fetch_api_create_response.py +++ b/src/browserbase/types/fetch_api_create_response.py @@ -1,6 +1,6 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -from typing import Dict +from typing import Dict, Union from pydantic import Field as FieldInfo @@ -13,8 +13,12 @@ class FetchAPICreateResponse(BaseModel): id: str """Unique identifier for the fetch request""" - content: str - """The response body content""" + content: Union[str, Dict[str, object]] + """The response body content. + + A string for `raw` and `markdown` formats; a structured object for `json` format + (the schema-extracted result). + """ content_type: str = FieldInfo(alias="contentType") """The MIME type of the response""" diff --git a/tests/api_resources/test_fetch_api.py b/tests/api_resources/test_fetch_api.py index b9a0455b..def3304d 100644 --- a/tests/api_resources/test_fetch_api.py +++ b/tests/api_resources/test_fetch_api.py @@ -30,7 +30,9 @@ def test_method_create_with_all_params(self, client: Browserbase) -> None: url="https://example.com", allow_insecure_ssl=True, allow_redirects=True, + format="raw", proxies=True, + schema={"foo": "bar"}, ) assert_matches_type(FetchAPICreateResponse, fetch_api, path=["response"]) @@ -77,7 +79,9 @@ async def test_method_create_with_all_params(self, async_client: AsyncBrowserbas url="https://example.com", allow_insecure_ssl=True, allow_redirects=True, + format="raw", proxies=True, + schema={"foo": "bar"}, ) assert_matches_type(FetchAPICreateResponse, fetch_api, path=["response"]) From d1f7c6d8a4078c4e62e1d15174238070ebd378dc Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 20 May 2026 20:38:51 +0000 Subject: [PATCH 317/330] feat(api): manual updates --- .stats.yml | 6 +- api.md | 13 + .../resources/sessions/__init__.py | 14 + src/browserbase/resources/sessions/replays.py | 266 ++++++++++++++++++ .../resources/sessions/sessions.py | 32 +++ src/browserbase/types/sessions/__init__.py | 1 + .../sessions/replay_retrieve_response.py | 25 ++ tests/api_resources/sessions/test_replays.py | 242 ++++++++++++++++ 8 files changed, 596 insertions(+), 3 deletions(-) create mode 100644 src/browserbase/resources/sessions/replays.py create mode 100644 src/browserbase/types/sessions/replay_retrieve_response.py create mode 100644 tests/api_resources/sessions/test_replays.py diff --git a/.stats.yml b/.stats.yml index 85445503..724c9191 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ -configured_endpoints: 21 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase/browserbase-3032c74460ad9c1c4b0a1b2d9107c556f292974a8ce12d525660a9cf31f10bc1.yml +configured_endpoints: 23 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase/browserbase-17193ab4cc450d38ce1bfdb1a571001c04b754ba2ff2d2bb2dcf4a898d6c6c41.yml openapi_spec_hash: 8ac1c673ce4e72b88bbbf30100b95e8f -config_hash: cf04ecfb8dad5fbd8b85be25d6e9ec55 +config_hash: 40fbac80e24faaa0dc19e93368bcd821 diff --git a/api.md b/api.md index b6066cb8..581574a3 100644 --- a/api.md +++ b/api.md @@ -128,3 +128,16 @@ from browserbase.types.sessions import UploadCreateResponse Methods: - client.sessions.uploads.create(id, \*\*params) -> UploadCreateResponse + +## Replays + +Types: + +```python +from browserbase.types.sessions import ReplayRetrieveResponse +``` + +Methods: + +- client.sessions.replays.retrieve(id) -> ReplayRetrieveResponse +- client.sessions.replays.retrieve_page(page_id, \*, id) -> BinaryAPIResponse diff --git a/src/browserbase/resources/sessions/__init__.py b/src/browserbase/resources/sessions/__init__.py index b3877e12..e66ee0ce 100644 --- a/src/browserbase/resources/sessions/__init__.py +++ b/src/browserbase/resources/sessions/__init__.py @@ -8,6 +8,14 @@ LogsResourceWithStreamingResponse, AsyncLogsResourceWithStreamingResponse, ) +from .replays import ( + ReplaysResource, + AsyncReplaysResource, + ReplaysResourceWithRawResponse, + AsyncReplaysResourceWithRawResponse, + ReplaysResourceWithStreamingResponse, + AsyncReplaysResourceWithStreamingResponse, +) from .uploads import ( UploadsResource, AsyncUploadsResource, @@ -66,6 +74,12 @@ "AsyncUploadsResourceWithRawResponse", "UploadsResourceWithStreamingResponse", "AsyncUploadsResourceWithStreamingResponse", + "ReplaysResource", + "AsyncReplaysResource", + "ReplaysResourceWithRawResponse", + "AsyncReplaysResourceWithRawResponse", + "ReplaysResourceWithStreamingResponse", + "AsyncReplaysResourceWithStreamingResponse", "SessionsResource", "AsyncSessionsResource", "SessionsResourceWithRawResponse", diff --git a/src/browserbase/resources/sessions/replays.py b/src/browserbase/resources/sessions/replays.py new file mode 100644 index 00000000..c9240356 --- /dev/null +++ b/src/browserbase/resources/sessions/replays.py @@ -0,0 +1,266 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import httpx + +from ..._types import Body, Query, Headers, NotGiven, not_given +from ..._utils import path_template +from ..._compat import cached_property +from ..._resource import SyncAPIResource, AsyncAPIResource +from ..._response import ( + BinaryAPIResponse, + AsyncBinaryAPIResponse, + StreamedBinaryAPIResponse, + AsyncStreamedBinaryAPIResponse, + to_raw_response_wrapper, + to_streamed_response_wrapper, + async_to_raw_response_wrapper, + to_custom_raw_response_wrapper, + async_to_streamed_response_wrapper, + to_custom_streamed_response_wrapper, + async_to_custom_raw_response_wrapper, + async_to_custom_streamed_response_wrapper, +) +from ..._base_client import make_request_options +from ...types.sessions.replay_retrieve_response import ReplayRetrieveResponse + +__all__ = ["ReplaysResource", "AsyncReplaysResource"] + + +class ReplaysResource(SyncAPIResource): + @cached_property + def with_raw_response(self) -> ReplaysResourceWithRawResponse: + """ + 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/sdk-python#accessing-raw-response-data-eg-headers + """ + return ReplaysResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> ReplaysResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/browserbase/sdk-python#with_streaming_response + """ + return ReplaysResourceWithStreamingResponse(self) + + def retrieve( + self, + 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, + ) -> ReplayRetrieveResponse: + """ + Returns page metadata for a session replay, including timing information and the + URL of each page's HLS playlist. + + 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 id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return self._get( + path_template("/v1/sessions/{id}/replays", id=id), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=ReplayRetrieveResponse, + ) + + def retrieve_page( + self, + page_id: str, + *, + 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, + ) -> BinaryAPIResponse: + """ + Returns an HLS VOD media playlist (.m3u8) for a specific page of a session + replay. + + 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 id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + if not page_id: + raise ValueError(f"Expected a non-empty value for `page_id` but received {page_id!r}") + extra_headers = {"Accept": "application/vnd.apple.mpegurl", **(extra_headers or {})} + return self._get( + path_template("/v1/sessions/{id}/replays/{page_id}", id=id, page_id=page_id), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=BinaryAPIResponse, + ) + + +class AsyncReplaysResource(AsyncAPIResource): + @cached_property + def with_raw_response(self) -> AsyncReplaysResourceWithRawResponse: + """ + 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/sdk-python#accessing-raw-response-data-eg-headers + """ + return AsyncReplaysResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncReplaysResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/browserbase/sdk-python#with_streaming_response + """ + return AsyncReplaysResourceWithStreamingResponse(self) + + async def retrieve( + self, + 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, + ) -> ReplayRetrieveResponse: + """ + Returns page metadata for a session replay, including timing information and the + URL of each page's HLS playlist. + + 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 id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return await self._get( + path_template("/v1/sessions/{id}/replays", id=id), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=ReplayRetrieveResponse, + ) + + async def retrieve_page( + self, + page_id: str, + *, + 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, + ) -> AsyncBinaryAPIResponse: + """ + Returns an HLS VOD media playlist (.m3u8) for a specific page of a session + replay. + + 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 id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + if not page_id: + raise ValueError(f"Expected a non-empty value for `page_id` but received {page_id!r}") + extra_headers = {"Accept": "application/vnd.apple.mpegurl", **(extra_headers or {})} + return await self._get( + path_template("/v1/sessions/{id}/replays/{page_id}", id=id, page_id=page_id), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=AsyncBinaryAPIResponse, + ) + + +class ReplaysResourceWithRawResponse: + def __init__(self, replays: ReplaysResource) -> None: + self._replays = replays + + self.retrieve = to_raw_response_wrapper( + replays.retrieve, + ) + self.retrieve_page = to_custom_raw_response_wrapper( + replays.retrieve_page, + BinaryAPIResponse, + ) + + +class AsyncReplaysResourceWithRawResponse: + def __init__(self, replays: AsyncReplaysResource) -> None: + self._replays = replays + + self.retrieve = async_to_raw_response_wrapper( + replays.retrieve, + ) + self.retrieve_page = async_to_custom_raw_response_wrapper( + replays.retrieve_page, + AsyncBinaryAPIResponse, + ) + + +class ReplaysResourceWithStreamingResponse: + def __init__(self, replays: ReplaysResource) -> None: + self._replays = replays + + self.retrieve = to_streamed_response_wrapper( + replays.retrieve, + ) + self.retrieve_page = to_custom_streamed_response_wrapper( + replays.retrieve_page, + StreamedBinaryAPIResponse, + ) + + +class AsyncReplaysResourceWithStreamingResponse: + def __init__(self, replays: AsyncReplaysResource) -> None: + self._replays = replays + + self.retrieve = async_to_streamed_response_wrapper( + replays.retrieve, + ) + self.retrieve_page = async_to_custom_streamed_response_wrapper( + replays.retrieve_page, + AsyncStreamedBinaryAPIResponse, + ) diff --git a/src/browserbase/resources/sessions/sessions.py b/src/browserbase/resources/sessions/sessions.py index ce3de98f..a54d7a72 100644 --- a/src/browserbase/resources/sessions/sessions.py +++ b/src/browserbase/resources/sessions/sessions.py @@ -16,6 +16,14 @@ AsyncLogsResourceWithStreamingResponse, ) from ...types import session_list_params, session_create_params, session_update_params +from .replays import ( + ReplaysResource, + AsyncReplaysResource, + ReplaysResourceWithRawResponse, + AsyncReplaysResourceWithRawResponse, + ReplaysResourceWithStreamingResponse, + AsyncReplaysResourceWithStreamingResponse, +) from .uploads import ( UploadsResource, AsyncUploadsResource, @@ -77,6 +85,10 @@ def recording(self) -> RecordingResource: def uploads(self) -> UploadsResource: return UploadsResource(self._client) + @cached_property + def replays(self) -> ReplaysResource: + return ReplaysResource(self._client) + @cached_property def with_raw_response(self) -> SessionsResourceWithRawResponse: """ @@ -349,6 +361,10 @@ def recording(self) -> AsyncRecordingResource: def uploads(self) -> AsyncUploadsResource: return AsyncUploadsResource(self._client) + @cached_property + def replays(self) -> AsyncReplaysResource: + return AsyncReplaysResource(self._client) + @cached_property def with_raw_response(self) -> AsyncSessionsResourceWithRawResponse: """ @@ -640,6 +656,10 @@ def recording(self) -> RecordingResourceWithRawResponse: def uploads(self) -> UploadsResourceWithRawResponse: return UploadsResourceWithRawResponse(self._sessions.uploads) + @cached_property + def replays(self) -> ReplaysResourceWithRawResponse: + return ReplaysResourceWithRawResponse(self._sessions.replays) + class AsyncSessionsResourceWithRawResponse: def __init__(self, sessions: AsyncSessionsResource) -> None: @@ -677,6 +697,10 @@ def recording(self) -> AsyncRecordingResourceWithRawResponse: def uploads(self) -> AsyncUploadsResourceWithRawResponse: return AsyncUploadsResourceWithRawResponse(self._sessions.uploads) + @cached_property + def replays(self) -> AsyncReplaysResourceWithRawResponse: + return AsyncReplaysResourceWithRawResponse(self._sessions.replays) + class SessionsResourceWithStreamingResponse: def __init__(self, sessions: SessionsResource) -> None: @@ -714,6 +738,10 @@ def recording(self) -> RecordingResourceWithStreamingResponse: def uploads(self) -> UploadsResourceWithStreamingResponse: return UploadsResourceWithStreamingResponse(self._sessions.uploads) + @cached_property + def replays(self) -> ReplaysResourceWithStreamingResponse: + return ReplaysResourceWithStreamingResponse(self._sessions.replays) + class AsyncSessionsResourceWithStreamingResponse: def __init__(self, sessions: AsyncSessionsResource) -> None: @@ -750,3 +778,7 @@ def recording(self) -> AsyncRecordingResourceWithStreamingResponse: @cached_property def uploads(self) -> AsyncUploadsResourceWithStreamingResponse: return AsyncUploadsResourceWithStreamingResponse(self._sessions.uploads) + + @cached_property + def replays(self) -> AsyncReplaysResourceWithStreamingResponse: + return AsyncReplaysResourceWithStreamingResponse(self._sessions.replays) diff --git a/src/browserbase/types/sessions/__init__.py b/src/browserbase/types/sessions/__init__.py index 0cef6b19..c7ea4671 100644 --- a/src/browserbase/types/sessions/__init__.py +++ b/src/browserbase/types/sessions/__init__.py @@ -7,4 +7,5 @@ from .session_recording import SessionRecording as SessionRecording from .upload_create_params import UploadCreateParams as UploadCreateParams from .upload_create_response import UploadCreateResponse as UploadCreateResponse +from .replay_retrieve_response import ReplayRetrieveResponse as ReplayRetrieveResponse from .recording_retrieve_response import RecordingRetrieveResponse as RecordingRetrieveResponse diff --git a/src/browserbase/types/sessions/replay_retrieve_response.py b/src/browserbase/types/sessions/replay_retrieve_response.py new file mode 100644 index 00000000..ec16398a --- /dev/null +++ b/src/browserbase/types/sessions/replay_retrieve_response.py @@ -0,0 +1,25 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List + +from pydantic import Field as FieldInfo + +from ..._models import BaseModel + +__all__ = ["ReplayRetrieveResponse", "Page"] + + +class Page(BaseModel): + end_time_ms: int = FieldInfo(alias="endTimeMs") + + page_id: str = FieldInfo(alias="pageId") + + start_time_ms: int = FieldInfo(alias="startTimeMs") + + url: str + + +class ReplayRetrieveResponse(BaseModel): + page_count: int = FieldInfo(alias="pageCount") + + pages: List[Page] diff --git a/tests/api_resources/sessions/test_replays.py b/tests/api_resources/sessions/test_replays.py new file mode 100644 index 00000000..a82c7880 --- /dev/null +++ b/tests/api_resources/sessions/test_replays.py @@ -0,0 +1,242 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import os +from typing import Any, cast + +import httpx +import pytest +from respx import MockRouter + +from browserbase import Browserbase, AsyncBrowserbase +from tests.utils import assert_matches_type +from browserbase._response import ( + BinaryAPIResponse, + AsyncBinaryAPIResponse, + StreamedBinaryAPIResponse, + AsyncStreamedBinaryAPIResponse, +) +from browserbase.types.sessions import ReplayRetrieveResponse + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + + +class TestReplays: + parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) + + @parametrize + def test_method_retrieve(self, client: Browserbase) -> None: + replay = client.sessions.replays.retrieve( + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + assert_matches_type(ReplayRetrieveResponse, replay, path=["response"]) + + @parametrize + def test_raw_response_retrieve(self, client: Browserbase) -> None: + response = client.sessions.replays.with_raw_response.retrieve( + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + replay = response.parse() + assert_matches_type(ReplayRetrieveResponse, replay, path=["response"]) + + @parametrize + def test_streaming_response_retrieve(self, client: Browserbase) -> None: + with client.sessions.replays.with_streaming_response.retrieve( + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + replay = response.parse() + assert_matches_type(ReplayRetrieveResponse, replay, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_retrieve(self, client: Browserbase) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.sessions.replays.with_raw_response.retrieve( + "", + ) + + @parametrize + @pytest.mark.respx(base_url=base_url) + def test_method_retrieve_page(self, client: Browserbase, respx_mock: MockRouter) -> None: + respx_mock.get("/v1/sessions/182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e/replays/090").mock( + return_value=httpx.Response(200, json={"foo": "bar"}) + ) + replay = client.sessions.replays.retrieve_page( + page_id="090", + id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + assert replay.is_closed + assert replay.json() == {"foo": "bar"} + assert cast(Any, replay.is_closed) is True + assert isinstance(replay, BinaryAPIResponse) + + @parametrize + @pytest.mark.respx(base_url=base_url) + def test_raw_response_retrieve_page(self, client: Browserbase, respx_mock: MockRouter) -> None: + respx_mock.get("/v1/sessions/182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e/replays/090").mock( + return_value=httpx.Response(200, json={"foo": "bar"}) + ) + + replay = client.sessions.replays.with_raw_response.retrieve_page( + page_id="090", + id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + + assert replay.is_closed is True + assert replay.http_request.headers.get("X-Stainless-Lang") == "python" + assert replay.json() == {"foo": "bar"} + assert isinstance(replay, BinaryAPIResponse) + + @parametrize + @pytest.mark.respx(base_url=base_url) + def test_streaming_response_retrieve_page(self, client: Browserbase, respx_mock: MockRouter) -> None: + respx_mock.get("/v1/sessions/182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e/replays/090").mock( + return_value=httpx.Response(200, json={"foo": "bar"}) + ) + with client.sessions.replays.with_streaming_response.retrieve_page( + page_id="090", + id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) as replay: + assert not replay.is_closed + assert replay.http_request.headers.get("X-Stainless-Lang") == "python" + + assert replay.json() == {"foo": "bar"} + assert cast(Any, replay.is_closed) is True + assert isinstance(replay, StreamedBinaryAPIResponse) + + assert cast(Any, replay.is_closed) is True + + @parametrize + @pytest.mark.respx(base_url=base_url) + def test_path_params_retrieve_page(self, client: Browserbase) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.sessions.replays.with_raw_response.retrieve_page( + page_id="090", + id="", + ) + + with pytest.raises(ValueError, match=r"Expected a non-empty value for `page_id` but received ''"): + client.sessions.replays.with_raw_response.retrieve_page( + page_id="", + id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + + +class TestAsyncReplays: + parametrize = pytest.mark.parametrize( + "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] + ) + + @parametrize + async def test_method_retrieve(self, async_client: AsyncBrowserbase) -> None: + replay = await async_client.sessions.replays.retrieve( + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + assert_matches_type(ReplayRetrieveResponse, replay, path=["response"]) + + @parametrize + async def test_raw_response_retrieve(self, async_client: AsyncBrowserbase) -> None: + response = await async_client.sessions.replays.with_raw_response.retrieve( + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + replay = await response.parse() + assert_matches_type(ReplayRetrieveResponse, replay, path=["response"]) + + @parametrize + async def test_streaming_response_retrieve(self, async_client: AsyncBrowserbase) -> None: + async with async_client.sessions.replays.with_streaming_response.retrieve( + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + replay = await response.parse() + assert_matches_type(ReplayRetrieveResponse, replay, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_retrieve(self, async_client: AsyncBrowserbase) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.sessions.replays.with_raw_response.retrieve( + "", + ) + + @parametrize + @pytest.mark.respx(base_url=base_url) + async def test_method_retrieve_page(self, async_client: AsyncBrowserbase, respx_mock: MockRouter) -> None: + respx_mock.get("/v1/sessions/182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e/replays/090").mock( + return_value=httpx.Response(200, json={"foo": "bar"}) + ) + replay = await async_client.sessions.replays.retrieve_page( + page_id="090", + id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + assert replay.is_closed + assert await replay.json() == {"foo": "bar"} + assert cast(Any, replay.is_closed) is True + assert isinstance(replay, AsyncBinaryAPIResponse) + + @parametrize + @pytest.mark.respx(base_url=base_url) + async def test_raw_response_retrieve_page(self, async_client: AsyncBrowserbase, respx_mock: MockRouter) -> None: + respx_mock.get("/v1/sessions/182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e/replays/090").mock( + return_value=httpx.Response(200, json={"foo": "bar"}) + ) + + replay = await async_client.sessions.replays.with_raw_response.retrieve_page( + page_id="090", + id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + + assert replay.is_closed is True + assert replay.http_request.headers.get("X-Stainless-Lang") == "python" + assert await replay.json() == {"foo": "bar"} + assert isinstance(replay, AsyncBinaryAPIResponse) + + @parametrize + @pytest.mark.respx(base_url=base_url) + async def test_streaming_response_retrieve_page( + self, async_client: AsyncBrowserbase, respx_mock: MockRouter + ) -> None: + respx_mock.get("/v1/sessions/182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e/replays/090").mock( + return_value=httpx.Response(200, json={"foo": "bar"}) + ) + async with async_client.sessions.replays.with_streaming_response.retrieve_page( + page_id="090", + id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) as replay: + assert not replay.is_closed + assert replay.http_request.headers.get("X-Stainless-Lang") == "python" + + assert await replay.json() == {"foo": "bar"} + assert cast(Any, replay.is_closed) is True + assert isinstance(replay, AsyncStreamedBinaryAPIResponse) + + assert cast(Any, replay.is_closed) is True + + @parametrize + @pytest.mark.respx(base_url=base_url) + async def test_path_params_retrieve_page(self, async_client: AsyncBrowserbase) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.sessions.replays.with_raw_response.retrieve_page( + page_id="090", + id="", + ) + + with pytest.raises(ValueError, match=r"Expected a non-empty value for `page_id` but received ''"): + await async_client.sessions.replays.with_raw_response.retrieve_page( + page_id="", + id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) From f7d3b4b998aba5eda365b996da48732c5dfcb4fe Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 20 May 2026 21:33:17 +0000 Subject: [PATCH 318/330] codegen metadata --- .stats.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index 724c9191..f40d1167 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 23 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase/browserbase-17193ab4cc450d38ce1bfdb1a571001c04b754ba2ff2d2bb2dcf4a898d6c6c41.yml -openapi_spec_hash: 8ac1c673ce4e72b88bbbf30100b95e8f +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase/browserbase-b2831c9c836f039762834825afdc20569587a825d29ac5c3748c78b009bf059b.yml +openapi_spec_hash: dd85a934900cb6583f12ebf6117be884 config_hash: 40fbac80e24faaa0dc19e93368bcd821 From f507ef87d3cfb70fc5ac50d1e4277d9a2bf78131 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 20 May 2026 22:43:17 +0000 Subject: [PATCH 319/330] chore(internal): version bump --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/browserbase/_version.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index eb4e0dba..caf14871 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "1.10.0" + ".": "1.11.0" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 68a1daeb..fbff452b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "browserbase" -version = "1.10.0" +version = "1.11.0" description = "The official Python library for the Browserbase API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/browserbase/_version.py b/src/browserbase/_version.py index c811ccc9..1184ad60 100644 --- a/src/browserbase/_version.py +++ b/src/browserbase/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "browserbase" -__version__ = "1.10.0" # x-release-please-version +__version__ = "1.11.0" # x-release-please-version From 34312b726001c9921b8435516d23209239f4fd5c Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 21 May 2026 20:11:56 +0000 Subject: [PATCH 320/330] codegen metadata --- .stats.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index f40d1167..0e228e1f 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 23 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase/browserbase-b2831c9c836f039762834825afdc20569587a825d29ac5c3748c78b009bf059b.yml -openapi_spec_hash: dd85a934900cb6583f12ebf6117be884 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase/browserbase-064acee5b1a935d704b776ae56c254b445b6d8314beccf2f9c6d7068d30a324e.yml +openapi_spec_hash: a9fae77266f1f3c17fabc2c69ae1d165 config_hash: 40fbac80e24faaa0dc19e93368bcd821 From 580a84560bf597d6f16848b59496c9ed559b7033 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 5 Jun 2026 18:46:50 +0000 Subject: [PATCH 321/330] codegen metadata --- .stats.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index 0e228e1f..635b0172 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 23 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase/browserbase-064acee5b1a935d704b776ae56c254b445b6d8314beccf2f9c6d7068d30a324e.yml -openapi_spec_hash: a9fae77266f1f3c17fabc2c69ae1d165 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase/browserbase-e558ae375c808495dd9e1ed1e305f8933d9418d380c395412764530d74d74895.yml +openapi_spec_hash: 518fdefff1eabc4bb8a3b54ddf7fa293 config_hash: 40fbac80e24faaa0dc19e93368bcd821 From fe501e6d4249f610db439781c13cd2b6684cb5d1 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 5 Jun 2026 19:56:15 +0000 Subject: [PATCH 322/330] feat: [CORE-2194][apps/api] Add BYOC (cert) CRUD to SDKs --- .stats.yml | 6 +- README.md | 2 +- api.md | 15 + src/browserbase/_client.py | 39 +- src/browserbase/resources/__init__.py | 14 + src/browserbase/resources/certificates.py | 389 ++++++++++++++++++ src/browserbase/types/__init__.py | 3 + src/browserbase/types/certificate.py | 20 + .../types/certificate_create_params.py | 13 + .../types/certificate_list_response.py | 10 + tests/api_resources/test_certificates.py | 288 +++++++++++++ 11 files changed, 794 insertions(+), 5 deletions(-) create mode 100644 src/browserbase/resources/certificates.py create mode 100644 src/browserbase/types/certificate.py create mode 100644 src/browserbase/types/certificate_create_params.py create mode 100644 src/browserbase/types/certificate_list_response.py create mode 100644 tests/api_resources/test_certificates.py diff --git a/.stats.yml b/.stats.yml index 635b0172..17cb18b8 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ -configured_endpoints: 23 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase/browserbase-e558ae375c808495dd9e1ed1e305f8933d9418d380c395412764530d74d74895.yml +configured_endpoints: 27 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase/browserbase-9bab373fc62ce19147560ed3ec29fe09ad59d9a5b406d9ed21a22f15a511d9cb.yml openapi_spec_hash: 518fdefff1eabc4bb8a3b54ddf7fa293 -config_hash: 40fbac80e24faaa0dc19e93368bcd821 +config_hash: d4b0c534eaf7665ea25168e0e824c9d3 diff --git a/README.md b/README.md index 42bfedbf..537caa71 100644 --- a/README.md +++ b/README.md @@ -134,7 +134,7 @@ from browserbase import Browserbase client = Browserbase() -client.extensions.create( +client.certificates.create( file=Path("/path/to/file"), ) ``` diff --git a/api.md b/api.md index 581574a3..c3a3f4eb 100644 --- a/api.md +++ b/api.md @@ -1,3 +1,18 @@ +# Certificates + +Types: + +```python +from browserbase.types import Certificate, CertificateListResponse +``` + +Methods: + +- client.certificates.create(\*\*params) -> Certificate +- client.certificates.retrieve(id) -> Certificate +- client.certificates.list() -> CertificateListResponse +- client.certificates.delete(id) -> None + # Contexts Types: diff --git a/src/browserbase/_client.py b/src/browserbase/_client.py index b64caef8..d86814d7 100644 --- a/src/browserbase/_client.py +++ b/src/browserbase/_client.py @@ -35,12 +35,13 @@ ) if TYPE_CHECKING: - from .resources import search, contexts, projects, sessions, fetch_api, extensions + from .resources import search, contexts, projects, sessions, fetch_api, extensions, certificates from .resources.search import SearchResource, AsyncSearchResource from .resources.contexts import ContextsResource, AsyncContextsResource from .resources.projects import ProjectsResource, AsyncProjectsResource from .resources.fetch_api import FetchAPIResource, AsyncFetchAPIResource from .resources.extensions import ExtensionsResource, AsyncExtensionsResource + from .resources.certificates import CertificatesResource, AsyncCertificatesResource from .resources.sessions.sessions import SessionsResource, AsyncSessionsResource __all__ = [ @@ -119,6 +120,12 @@ def __init__( _strict_response_validation=_strict_response_validation, ) + @cached_property + def certificates(self) -> CertificatesResource: + from .resources.certificates import CertificatesResource + + return CertificatesResource(self) + @cached_property def contexts(self) -> ContextsResource: from .resources.contexts import ContextsResource @@ -332,6 +339,12 @@ def __init__( _strict_response_validation=_strict_response_validation, ) + @cached_property + def certificates(self) -> AsyncCertificatesResource: + from .resources.certificates import AsyncCertificatesResource + + return AsyncCertificatesResource(self) + @cached_property def contexts(self) -> AsyncContextsResource: from .resources.contexts import AsyncContextsResource @@ -487,6 +500,12 @@ class BrowserbaseWithRawResponse: def __init__(self, client: Browserbase) -> None: self._client = client + @cached_property + def certificates(self) -> certificates.CertificatesResourceWithRawResponse: + from .resources.certificates import CertificatesResourceWithRawResponse + + return CertificatesResourceWithRawResponse(self._client.certificates) + @cached_property def contexts(self) -> contexts.ContextsResourceWithRawResponse: from .resources.contexts import ContextsResourceWithRawResponse @@ -530,6 +549,12 @@ class AsyncBrowserbaseWithRawResponse: def __init__(self, client: AsyncBrowserbase) -> None: self._client = client + @cached_property + def certificates(self) -> certificates.AsyncCertificatesResourceWithRawResponse: + from .resources.certificates import AsyncCertificatesResourceWithRawResponse + + return AsyncCertificatesResourceWithRawResponse(self._client.certificates) + @cached_property def contexts(self) -> contexts.AsyncContextsResourceWithRawResponse: from .resources.contexts import AsyncContextsResourceWithRawResponse @@ -573,6 +598,12 @@ class BrowserbaseWithStreamedResponse: def __init__(self, client: Browserbase) -> None: self._client = client + @cached_property + def certificates(self) -> certificates.CertificatesResourceWithStreamingResponse: + from .resources.certificates import CertificatesResourceWithStreamingResponse + + return CertificatesResourceWithStreamingResponse(self._client.certificates) + @cached_property def contexts(self) -> contexts.ContextsResourceWithStreamingResponse: from .resources.contexts import ContextsResourceWithStreamingResponse @@ -616,6 +647,12 @@ class AsyncBrowserbaseWithStreamedResponse: def __init__(self, client: AsyncBrowserbase) -> None: self._client = client + @cached_property + def certificates(self) -> certificates.AsyncCertificatesResourceWithStreamingResponse: + from .resources.certificates import AsyncCertificatesResourceWithStreamingResponse + + return AsyncCertificatesResourceWithStreamingResponse(self._client.certificates) + @cached_property def contexts(self) -> contexts.AsyncContextsResourceWithStreamingResponse: from .resources.contexts import AsyncContextsResourceWithStreamingResponse diff --git a/src/browserbase/resources/__init__.py b/src/browserbase/resources/__init__.py index 83a8788d..6bd09881 100644 --- a/src/browserbase/resources/__init__.py +++ b/src/browserbase/resources/__init__.py @@ -48,8 +48,22 @@ ExtensionsResourceWithStreamingResponse, AsyncExtensionsResourceWithStreamingResponse, ) +from .certificates import ( + CertificatesResource, + AsyncCertificatesResource, + CertificatesResourceWithRawResponse, + AsyncCertificatesResourceWithRawResponse, + CertificatesResourceWithStreamingResponse, + AsyncCertificatesResourceWithStreamingResponse, +) __all__ = [ + "CertificatesResource", + "AsyncCertificatesResource", + "CertificatesResourceWithRawResponse", + "AsyncCertificatesResourceWithRawResponse", + "CertificatesResourceWithStreamingResponse", + "AsyncCertificatesResourceWithStreamingResponse", "ContextsResource", "AsyncContextsResource", "ContextsResourceWithRawResponse", diff --git a/src/browserbase/resources/certificates.py b/src/browserbase/resources/certificates.py new file mode 100644 index 00000000..a617e776 --- /dev/null +++ b/src/browserbase/resources/certificates.py @@ -0,0 +1,389 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Mapping, cast + +import httpx + +from ..types import certificate_create_params +from .._files import deepcopy_with_paths +from .._types import Body, Query, Headers, NoneType, NotGiven, FileTypes, not_given +from .._utils import extract_files, path_template, maybe_transform, 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.certificate import Certificate +from ..types.certificate_list_response import CertificateListResponse + +__all__ = ["CertificatesResource", "AsyncCertificatesResource"] + + +class CertificatesResource(SyncAPIResource): + @cached_property + def with_raw_response(self) -> CertificatesResourceWithRawResponse: + """ + 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/sdk-python#accessing-raw-response-data-eg-headers + """ + return CertificatesResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> CertificatesResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/browserbase/sdk-python#with_streaming_response + """ + return CertificatesResourceWithStreamingResponse(self) + + def create( + self, + *, + file: FileTypes, + # 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, + ) -> Certificate: + """ + Upload a Certificate + + 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 + """ + body = deepcopy_with_paths({"file": file}, [["file"]]) + files = extract_files(cast(Mapping[str, object], body), paths=[["file"]]) + # It should be noted that the actual Content-Type header that will be + # sent to the server will contain a `boundary` parameter, e.g. + # multipart/form-data; boundary=---abc-- + extra_headers = {"Content-Type": "multipart/form-data", **(extra_headers or {})} + return self._post( + "/v1/certificates", + body=maybe_transform(body, certificate_create_params.CertificateCreateParams), + files=files, + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=Certificate, + ) + + def retrieve( + self, + 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, + ) -> Certificate: + """ + Get a Certificate + + 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 id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return self._get( + path_template("/v1/certificates/{id}", id=id), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=Certificate, + ) + + def list( + self, + *, + # 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, + ) -> CertificateListResponse: + """List Certificates""" + return self._get( + "/v1/certificates", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=CertificateListResponse, + ) + + def delete( + self, + 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, + ) -> None: + """ + Delete a Certificate + + 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 id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + extra_headers = {"Accept": "*/*", **(extra_headers or {})} + return self._delete( + path_template("/v1/certificates/{id}", id=id), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=NoneType, + ) + + +class AsyncCertificatesResource(AsyncAPIResource): + @cached_property + def with_raw_response(self) -> AsyncCertificatesResourceWithRawResponse: + """ + 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/sdk-python#accessing-raw-response-data-eg-headers + """ + return AsyncCertificatesResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncCertificatesResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/browserbase/sdk-python#with_streaming_response + """ + return AsyncCertificatesResourceWithStreamingResponse(self) + + async def create( + self, + *, + file: FileTypes, + # 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, + ) -> Certificate: + """ + Upload a Certificate + + 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 + """ + body = deepcopy_with_paths({"file": file}, [["file"]]) + files = extract_files(cast(Mapping[str, object], body), paths=[["file"]]) + # It should be noted that the actual Content-Type header that will be + # sent to the server will contain a `boundary` parameter, e.g. + # multipart/form-data; boundary=---abc-- + extra_headers = {"Content-Type": "multipart/form-data", **(extra_headers or {})} + return await self._post( + "/v1/certificates", + body=await async_maybe_transform(body, certificate_create_params.CertificateCreateParams), + files=files, + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=Certificate, + ) + + async def retrieve( + self, + 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, + ) -> Certificate: + """ + Get a Certificate + + 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 id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return await self._get( + path_template("/v1/certificates/{id}", id=id), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=Certificate, + ) + + async def list( + self, + *, + # 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, + ) -> CertificateListResponse: + """List Certificates""" + return await self._get( + "/v1/certificates", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=CertificateListResponse, + ) + + async def delete( + self, + 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, + ) -> None: + """ + Delete a Certificate + + 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 id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + extra_headers = {"Accept": "*/*", **(extra_headers or {})} + return await self._delete( + path_template("/v1/certificates/{id}", id=id), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=NoneType, + ) + + +class CertificatesResourceWithRawResponse: + def __init__(self, certificates: CertificatesResource) -> None: + self._certificates = certificates + + self.create = to_raw_response_wrapper( + certificates.create, + ) + self.retrieve = to_raw_response_wrapper( + certificates.retrieve, + ) + self.list = to_raw_response_wrapper( + certificates.list, + ) + self.delete = to_raw_response_wrapper( + certificates.delete, + ) + + +class AsyncCertificatesResourceWithRawResponse: + def __init__(self, certificates: AsyncCertificatesResource) -> None: + self._certificates = certificates + + self.create = async_to_raw_response_wrapper( + certificates.create, + ) + self.retrieve = async_to_raw_response_wrapper( + certificates.retrieve, + ) + self.list = async_to_raw_response_wrapper( + certificates.list, + ) + self.delete = async_to_raw_response_wrapper( + certificates.delete, + ) + + +class CertificatesResourceWithStreamingResponse: + def __init__(self, certificates: CertificatesResource) -> None: + self._certificates = certificates + + self.create = to_streamed_response_wrapper( + certificates.create, + ) + self.retrieve = to_streamed_response_wrapper( + certificates.retrieve, + ) + self.list = to_streamed_response_wrapper( + certificates.list, + ) + self.delete = to_streamed_response_wrapper( + certificates.delete, + ) + + +class AsyncCertificatesResourceWithStreamingResponse: + def __init__(self, certificates: AsyncCertificatesResource) -> None: + self._certificates = certificates + + self.create = async_to_streamed_response_wrapper( + certificates.create, + ) + self.retrieve = async_to_streamed_response_wrapper( + certificates.retrieve, + ) + self.list = async_to_streamed_response_wrapper( + certificates.list, + ) + self.delete = async_to_streamed_response_wrapper( + certificates.delete, + ) diff --git a/src/browserbase/types/__init__.py b/src/browserbase/types/__init__.py index 0a9a3b84..72d472ac 100644 --- a/src/browserbase/types/__init__.py +++ b/src/browserbase/types/__init__.py @@ -6,6 +6,7 @@ from .project import Project as Project from .session import Session as Session from .extension import Extension as Extension +from .certificate import Certificate as Certificate from .project_usage import ProjectUsage as ProjectUsage from .search_web_params import SearchWebParams as SearchWebParams from .session_live_urls import SessionLiveURLs as SessionLiveURLs @@ -21,5 +22,7 @@ from .extension_create_params import ExtensionCreateParams as ExtensionCreateParams from .fetch_api_create_params import FetchAPICreateParams as FetchAPICreateParams from .session_create_response import SessionCreateResponse as SessionCreateResponse +from .certificate_create_params import CertificateCreateParams as CertificateCreateParams +from .certificate_list_response import CertificateListResponse as CertificateListResponse from .fetch_api_create_response import FetchAPICreateResponse as FetchAPICreateResponse from .session_retrieve_response import SessionRetrieveResponse as SessionRetrieveResponse diff --git a/src/browserbase/types/certificate.py b/src/browserbase/types/certificate.py new file mode 100644 index 00000000..0f59e105 --- /dev/null +++ b/src/browserbase/types/certificate.py @@ -0,0 +1,20 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from datetime import datetime + +from pydantic import Field as FieldInfo + +from .._models import BaseModel + +__all__ = ["Certificate"] + + +class Certificate(BaseModel): + id: str + + created_at: datetime = FieldInfo(alias="createdAt") + + project_id: str = FieldInfo(alias="projectId") + """The Project ID linked to the uploaded Certificate.""" + + updated_at: datetime = FieldInfo(alias="updatedAt") diff --git a/src/browserbase/types/certificate_create_params.py b/src/browserbase/types/certificate_create_params.py new file mode 100644 index 00000000..577c0e07 --- /dev/null +++ b/src/browserbase/types/certificate_create_params.py @@ -0,0 +1,13 @@ +# 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 FileTypes + +__all__ = ["CertificateCreateParams"] + + +class CertificateCreateParams(TypedDict, total=False): + file: Required[FileTypes] diff --git a/src/browserbase/types/certificate_list_response.py b/src/browserbase/types/certificate_list_response.py new file mode 100644 index 00000000..7a8a8f06 --- /dev/null +++ b/src/browserbase/types/certificate_list_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 .certificate import Certificate + +__all__ = ["CertificateListResponse"] + +CertificateListResponse: TypeAlias = List[Certificate] diff --git a/tests/api_resources/test_certificates.py b/tests/api_resources/test_certificates.py new file mode 100644 index 00000000..aaa767ed --- /dev/null +++ b/tests/api_resources/test_certificates.py @@ -0,0 +1,288 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import os +from typing import Any, cast + +import pytest + +from browserbase import Browserbase, AsyncBrowserbase +from tests.utils import assert_matches_type +from browserbase.types import Certificate, CertificateListResponse + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + + +class TestCertificates: + parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) + + @parametrize + def test_method_create(self, client: Browserbase) -> None: + certificate = client.certificates.create( + file=b"Example data", + ) + assert_matches_type(Certificate, certificate, path=["response"]) + + @parametrize + def test_raw_response_create(self, client: Browserbase) -> None: + response = client.certificates.with_raw_response.create( + file=b"Example data", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + certificate = response.parse() + assert_matches_type(Certificate, certificate, path=["response"]) + + @parametrize + def test_streaming_response_create(self, client: Browserbase) -> None: + with client.certificates.with_streaming_response.create( + file=b"Example data", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + certificate = response.parse() + assert_matches_type(Certificate, certificate, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_method_retrieve(self, client: Browserbase) -> None: + certificate = client.certificates.retrieve( + "id", + ) + assert_matches_type(Certificate, certificate, path=["response"]) + + @parametrize + def test_raw_response_retrieve(self, client: Browserbase) -> None: + response = client.certificates.with_raw_response.retrieve( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + certificate = response.parse() + assert_matches_type(Certificate, certificate, path=["response"]) + + @parametrize + def test_streaming_response_retrieve(self, client: Browserbase) -> None: + with client.certificates.with_streaming_response.retrieve( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + certificate = response.parse() + assert_matches_type(Certificate, certificate, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_retrieve(self, client: Browserbase) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.certificates.with_raw_response.retrieve( + "", + ) + + @parametrize + def test_method_list(self, client: Browserbase) -> None: + certificate = client.certificates.list() + assert_matches_type(CertificateListResponse, certificate, path=["response"]) + + @parametrize + def test_raw_response_list(self, client: Browserbase) -> None: + response = client.certificates.with_raw_response.list() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + certificate = response.parse() + assert_matches_type(CertificateListResponse, certificate, path=["response"]) + + @parametrize + def test_streaming_response_list(self, client: Browserbase) -> None: + with client.certificates.with_streaming_response.list() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + certificate = response.parse() + assert_matches_type(CertificateListResponse, certificate, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_method_delete(self, client: Browserbase) -> None: + certificate = client.certificates.delete( + "id", + ) + assert certificate is None + + @parametrize + def test_raw_response_delete(self, client: Browserbase) -> None: + response = client.certificates.with_raw_response.delete( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + certificate = response.parse() + assert certificate is None + + @parametrize + def test_streaming_response_delete(self, client: Browserbase) -> None: + with client.certificates.with_streaming_response.delete( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + certificate = response.parse() + assert certificate is None + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_delete(self, client: Browserbase) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.certificates.with_raw_response.delete( + "", + ) + + +class TestAsyncCertificates: + parametrize = pytest.mark.parametrize( + "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] + ) + + @parametrize + async def test_method_create(self, async_client: AsyncBrowserbase) -> None: + certificate = await async_client.certificates.create( + file=b"Example data", + ) + assert_matches_type(Certificate, certificate, path=["response"]) + + @parametrize + async def test_raw_response_create(self, async_client: AsyncBrowserbase) -> None: + response = await async_client.certificates.with_raw_response.create( + file=b"Example data", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + certificate = await response.parse() + assert_matches_type(Certificate, certificate, path=["response"]) + + @parametrize + async def test_streaming_response_create(self, async_client: AsyncBrowserbase) -> None: + async with async_client.certificates.with_streaming_response.create( + file=b"Example data", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + certificate = await response.parse() + assert_matches_type(Certificate, certificate, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_method_retrieve(self, async_client: AsyncBrowserbase) -> None: + certificate = await async_client.certificates.retrieve( + "id", + ) + assert_matches_type(Certificate, certificate, path=["response"]) + + @parametrize + async def test_raw_response_retrieve(self, async_client: AsyncBrowserbase) -> None: + response = await async_client.certificates.with_raw_response.retrieve( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + certificate = await response.parse() + assert_matches_type(Certificate, certificate, path=["response"]) + + @parametrize + async def test_streaming_response_retrieve(self, async_client: AsyncBrowserbase) -> None: + async with async_client.certificates.with_streaming_response.retrieve( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + certificate = await response.parse() + assert_matches_type(Certificate, certificate, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_retrieve(self, async_client: AsyncBrowserbase) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.certificates.with_raw_response.retrieve( + "", + ) + + @parametrize + async def test_method_list(self, async_client: AsyncBrowserbase) -> None: + certificate = await async_client.certificates.list() + assert_matches_type(CertificateListResponse, certificate, path=["response"]) + + @parametrize + async def test_raw_response_list(self, async_client: AsyncBrowserbase) -> None: + response = await async_client.certificates.with_raw_response.list() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + certificate = await response.parse() + assert_matches_type(CertificateListResponse, certificate, path=["response"]) + + @parametrize + async def test_streaming_response_list(self, async_client: AsyncBrowserbase) -> None: + async with async_client.certificates.with_streaming_response.list() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + certificate = await response.parse() + assert_matches_type(CertificateListResponse, certificate, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_method_delete(self, async_client: AsyncBrowserbase) -> None: + certificate = await async_client.certificates.delete( + "id", + ) + assert certificate is None + + @parametrize + async def test_raw_response_delete(self, async_client: AsyncBrowserbase) -> None: + response = await async_client.certificates.with_raw_response.delete( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + certificate = await response.parse() + assert certificate is None + + @parametrize + async def test_streaming_response_delete(self, async_client: AsyncBrowserbase) -> None: + async with async_client.certificates.with_streaming_response.delete( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + certificate = await response.parse() + assert certificate is None + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_delete(self, async_client: AsyncBrowserbase) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.certificates.with_raw_response.delete( + "", + ) From 9cf583d5a9620a107bed6c7d147fc7453255c0a4 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 5 Jun 2026 20:03:51 +0000 Subject: [PATCH 323/330] chore(internal): version bump --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/browserbase/_version.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index caf14871..de0960ab 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "1.11.0" + ".": "1.12.0" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index fbff452b..8d554f0b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "browserbase" -version = "1.11.0" +version = "1.12.0" description = "The official Python library for the Browserbase API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/browserbase/_version.py b/src/browserbase/_version.py index 1184ad60..156b5867 100644 --- a/src/browserbase/_version.py +++ b/src/browserbase/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "browserbase" -__version__ = "1.11.0" # x-release-please-version +__version__ = "1.12.0" # x-release-please-version From fc2fc2628a12220c7d9cdb98a3829ecc1590b810 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 8 Jun 2026 17:25:38 +0000 Subject: [PATCH 324/330] feat: [AI-2206][apps/api] Surface proxySettings.caCertificates in public SDK & docs --- .stats.yml | 4 ++-- src/browserbase/resources/sessions/sessions.py | 8 ++++++++ src/browserbase/types/session_create_params.py | 12 ++++++++++++ tests/api_resources/test_sessions.py | 2 ++ 4 files changed, 24 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index 17cb18b8..1ef2a27b 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 27 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase/browserbase-9bab373fc62ce19147560ed3ec29fe09ad59d9a5b406d9ed21a22f15a511d9cb.yml -openapi_spec_hash: 518fdefff1eabc4bb8a3b54ddf7fa293 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase/browserbase-f39b852755134d01a440f7c37701f6c5397f43d13740d9ba08739cae488382a7.yml +openapi_spec_hash: de6c25eebe5026d0fb9a4d7a93ec7718 config_hash: d4b0c534eaf7665ea25168e0e824c9d3 diff --git a/src/browserbase/resources/sessions/sessions.py b/src/browserbase/resources/sessions/sessions.py index a54d7a72..8cf81349 100644 --- a/src/browserbase/resources/sessions/sessions.py +++ b/src/browserbase/resources/sessions/sessions.py @@ -116,6 +116,7 @@ def create( keep_alive: bool | Omit = omit, project_id: str | Omit = omit, proxies: Union[Iterable[session_create_params.ProxiesUnionMember0], bool] | Omit = omit, + proxy_settings: session_create_params.ProxySettings | Omit = omit, region: Literal["us-west-2", "us-east-1", "eu-central-1", "ap-southeast-1"] | Omit = omit, api_timeout: int | Omit = omit, user_metadata: Dict[str, object] | Omit = omit, @@ -144,6 +145,8 @@ def create( proxies: Proxy configuration. Can be true for default proxy, or an array of proxy configurations. + proxy_settings: Supplementary proxy settings. Optional. + region: The region where the Session should run. api_timeout: Duration in seconds after which the session will automatically end. Defaults to @@ -169,6 +172,7 @@ def create( "keep_alive": keep_alive, "project_id": project_id, "proxies": proxies, + "proxy_settings": proxy_settings, "region": region, "api_timeout": api_timeout, "user_metadata": user_metadata, @@ -392,6 +396,7 @@ async def create( keep_alive: bool | Omit = omit, project_id: str | Omit = omit, proxies: Union[Iterable[session_create_params.ProxiesUnionMember0], bool] | Omit = omit, + proxy_settings: session_create_params.ProxySettings | Omit = omit, region: Literal["us-west-2", "us-east-1", "eu-central-1", "ap-southeast-1"] | Omit = omit, api_timeout: int | Omit = omit, user_metadata: Dict[str, object] | Omit = omit, @@ -420,6 +425,8 @@ async def create( proxies: Proxy configuration. Can be true for default proxy, or an array of proxy configurations. + proxy_settings: Supplementary proxy settings. Optional. + region: The region where the Session should run. api_timeout: Duration in seconds after which the session will automatically end. Defaults to @@ -445,6 +452,7 @@ async def create( "keep_alive": keep_alive, "project_id": project_id, "proxies": proxies, + "proxy_settings": proxy_settings, "region": region, "api_timeout": api_timeout, "user_metadata": user_metadata, diff --git a/src/browserbase/types/session_create_params.py b/src/browserbase/types/session_create_params.py index 3ff1d058..2d0d39ac 100644 --- a/src/browserbase/types/session_create_params.py +++ b/src/browserbase/types/session_create_params.py @@ -5,6 +5,7 @@ from typing import Dict, Union, Iterable from typing_extensions import Literal, Required, Annotated, TypeAlias, TypedDict +from .._types import SequenceNotStr from .._utils import PropertyInfo __all__ = [ @@ -17,6 +18,7 @@ "ProxiesUnionMember0BrowserbaseProxyConfigGeolocation", "ProxiesUnionMember0ExternalProxyConfig", "ProxiesUnionMember0NoneProxyConfig", + "ProxySettings", ] @@ -48,6 +50,9 @@ class SessionCreateParams(TypedDict, total=False): Can be true for default proxy, or an array of proxy configurations. """ + proxy_settings: Annotated[ProxySettings, PropertyInfo(alias="proxySettings")] + """Supplementary proxy settings. Optional.""" + region: Literal["us-west-2", "us-east-1", "eu-central-1", "ap-southeast-1"] """The region where the Session should run.""" @@ -201,3 +206,10 @@ class ProxiesUnionMember0NoneProxyConfig(TypedDict, total=False): ProxiesUnionMember0ExternalProxyConfig, ProxiesUnionMember0NoneProxyConfig, ] + + +class ProxySettings(TypedDict, total=False): + """Supplementary proxy settings. Optional.""" + + ca_certificates: Annotated[SequenceNotStr[str], PropertyInfo(alias="caCertificates")] + """The TLS certificate IDs to trust. Optional.""" diff --git a/tests/api_resources/test_sessions.py b/tests/api_resources/test_sessions.py index 41f2a0bd..fe6d486e 100644 --- a/tests/api_resources/test_sessions.py +++ b/tests/api_resources/test_sessions.py @@ -66,6 +66,7 @@ def test_method_create_with_all_params(self, client: Browserbase) -> None: }, } ], + proxy_settings={"ca_certificates": ["182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e"]}, region="us-west-2", api_timeout=60, user_metadata={"foo": "bar"}, @@ -301,6 +302,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncBrowserbas }, } ], + proxy_settings={"ca_certificates": ["182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e"]}, region="us-west-2", api_timeout=60, user_metadata={"foo": "bar"}, From 053b3eefb2fe368f30807c73b2f33f1f4ca6c66f Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 8 Jun 2026 19:41:10 +0000 Subject: [PATCH 325/330] chore(internal): version bump --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/browserbase/_version.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index de0960ab..f94eeca2 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "1.12.0" + ".": "1.13.0" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 8d554f0b..62816cb5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "browserbase" -version = "1.12.0" +version = "1.13.0" description = "The official Python library for the Browserbase API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/browserbase/_version.py b/src/browserbase/_version.py index 156b5867..5daf1b6a 100644 --- a/src/browserbase/_version.py +++ b/src/browserbase/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "browserbase" -__version__ = "1.12.0" # x-release-please-version +__version__ = "1.13.0" # x-release-please-version From 4c155c211d3df9c85e7abbb9b4fdf0470b3eb872 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 9 Jun 2026 17:53:34 +0000 Subject: [PATCH 326/330] codegen metadata --- .stats.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index 1ef2a27b..b6b58cb7 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 27 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase/browserbase-f39b852755134d01a440f7c37701f6c5397f43d13740d9ba08739cae488382a7.yml -openapi_spec_hash: de6c25eebe5026d0fb9a4d7a93ec7718 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase/browserbase-eaca39f859613c4a3a96aaaaadf8bd92e845dc7245eb78617988e14e14d6a554.yml +openapi_spec_hash: e66a466a5446a2d85781786768e6b257 config_hash: d4b0c534eaf7665ea25168e0e824c9d3 From 6f9a658eedd96ea7157265b6b7a2dc5f251a7178 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 10 Jun 2026 04:55:11 +0000 Subject: [PATCH 327/330] codegen metadata --- .stats.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index b6b58cb7..1b5592f5 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 27 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase/browserbase-eaca39f859613c4a3a96aaaaadf8bd92e845dc7245eb78617988e14e14d6a554.yml -openapi_spec_hash: e66a466a5446a2d85781786768e6b257 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase/browserbase-a896c28015c625eaa4efd6319489dc91c932a6ab17780c5e1bf3648bd4bed9c7.yml +openapi_spec_hash: a1ee972826c5331971d4d9b437f9e6d5 config_hash: d4b0c534eaf7665ea25168e0e824c9d3 From b515b343c06728658add95d899044419fcae7ff5 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 12 Jun 2026 19:28:48 +0000 Subject: [PATCH 328/330] docs: document and un-gate allowedDomains session setting --- .stats.yml | 4 ++-- src/browserbase/types/session_create_params.py | 12 ++++++++++++ tests/api_resources/test_sessions.py | 2 ++ 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index 1b5592f5..2fef781c 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 27 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase/browserbase-a896c28015c625eaa4efd6319489dc91c932a6ab17780c5e1bf3648bd4bed9c7.yml -openapi_spec_hash: a1ee972826c5331971d4d9b437f9e6d5 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase/browserbase-325727e382d1635cdcff3d81e93432c314290a644a5252095fc9d23350e8fbae.yml +openapi_spec_hash: 1c0874c5d6253bdf144e617203d788c7 config_hash: d4b0c534eaf7665ea25168e0e824c9d3 diff --git a/src/browserbase/types/session_create_params.py b/src/browserbase/types/session_create_params.py index 2d0d39ac..b01d4476 100644 --- a/src/browserbase/types/session_create_params.py +++ b/src/browserbase/types/session_create_params.py @@ -90,6 +90,18 @@ class BrowserSettings(TypedDict, total=False): advanced_stealth: Annotated[bool, PropertyInfo(alias="advancedStealth")] """Advanced Browser Stealth Mode""" + allowed_domains: Annotated[SequenceNotStr[str], PropertyInfo(alias="allowedDomains")] + """An optional list of allowed domains for the session. + + If you pass one or more domains, Browserbase restricts top-level (main-frame) + page navigations to the listed domains and their subdomains. For example, + `example.com` also permits `www.example.com` and `a.b.example.com`, but not + `notexample.com`. Matching is domain-based, not full-URL. An empty list (the + default) disables the restriction entirely. Browserbase enforces only main-frame + navigations; it does not block iframe/subframe loads or other in-page resource + requests (images, scripts, XHR, etc.). + """ + block_ads: Annotated[bool, PropertyInfo(alias="blockAds")] """Enable or disable ad blocking in the browser. Defaults to `false`.""" diff --git a/tests/api_resources/test_sessions.py b/tests/api_resources/test_sessions.py index fe6d486e..f83f3d25 100644 --- a/tests/api_resources/test_sessions.py +++ b/tests/api_resources/test_sessions.py @@ -33,6 +33,7 @@ def test_method_create_with_all_params(self, client: Browserbase) -> None: session = client.sessions.create( browser_settings={ "advanced_stealth": True, + "allowed_domains": ["string"], "block_ads": True, "captcha_image_selector": "captchaImageSelector", "captcha_input_selector": "captchaInputSelector", @@ -269,6 +270,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncBrowserbas session = await async_client.sessions.create( browser_settings={ "advanced_stealth": True, + "allowed_domains": ["string"], "block_ads": True, "captcha_image_selector": "captchaImageSelector", "captcha_input_selector": "captchaInputSelector", From 4ed64e8226399d4c6f92d240eb156d06d537fe68 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 23 Jun 2026 17:58:49 +0000 Subject: [PATCH 329/330] codegen metadata --- .stats.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index 2fef781c..794cad27 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 27 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase/browserbase-325727e382d1635cdcff3d81e93432c314290a644a5252095fc9d23350e8fbae.yml -openapi_spec_hash: 1c0874c5d6253bdf144e617203d788c7 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase/browserbase-e2a07a0599ab06b1a961e9aea89d73986a2bae8e6ad942a4f8c223f97ea1634e.yml +openapi_spec_hash: 24a300b03dc458bee0787397795b4fba config_hash: d4b0c534eaf7665ea25168e0e824c9d3 From 64f67f6e9f35cde6ec93d3b1d31856978b0ea579 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 30 Jun 2026 00:58:06 +0000 Subject: [PATCH 330/330] codegen metadata --- .stats.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index 794cad27..bf003c72 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 27 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase/browserbase-e2a07a0599ab06b1a961e9aea89d73986a2bae8e6ad942a4f8c223f97ea1634e.yml -openapi_spec_hash: 24a300b03dc458bee0787397795b4fba +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase/browserbase-2d7e6d469fabaa60c27cb2c9c986b5bc2cd5d33fe13d9bd4e9b8ccaafa5b04c3.yml +openapi_spec_hash: 473121b283812a3dfd866afe9b61dc7d config_hash: d4b0c534eaf7665ea25168e0e824c9d3