diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile deleted file mode 100644 index ff261ba..0000000 --- a/.devcontainer/Dockerfile +++ /dev/null @@ -1,9 +0,0 @@ -ARG VARIANT="3.9" -FROM mcr.microsoft.com/vscode/devcontainers/python:0-${VARIANT} - -USER vscode - -RUN curl -sSf https://rye.astral.sh/get | RYE_VERSION="0.44.0" RYE_INSTALL_OPTION="--yes" bash -ENV PATH=/home/vscode/.rye/shims:$PATH - -RUN echo "[[ -d .venv ]] && source .venv/bin/activate || export PATH=\$PATH" >> /home/vscode/.bashrc diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json deleted file mode 100644 index c17fdc1..0000000 --- a/.devcontainer/devcontainer.json +++ /dev/null @@ -1,43 +0,0 @@ -// For format details, see https://aka.ms/devcontainer.json. For config options, see the -// README at: https://github.com/devcontainers/templates/tree/main/src/debian -{ - "name": "Debian", - "build": { - "dockerfile": "Dockerfile", - "context": ".." - }, - - "postStartCommand": "rye sync --all-features", - - "customizations": { - "vscode": { - "extensions": [ - "ms-python.python" - ], - "settings": { - "terminal.integrated.shell.linux": "/bin/bash", - "python.pythonPath": ".venv/bin/python", - "python.defaultInterpreterPath": ".venv/bin/python", - "python.typeChecking": "basic", - "terminal.integrated.env.linux": { - "PATH": "/home/vscode/.rye/shims:${env:PATH}" - } - } - } - }, - "features": { - "ghcr.io/devcontainers/features/node:1": {} - } - - // Features to add to the dev container. More info: https://containers.dev/features. - // "features": {}, - - // Use 'forwardPorts' to make a list of ports inside the container available locally. - // "forwardPorts": [], - - // Configure tool-specific properties. - // "customizations": {}, - - // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. - // "remoteUser": "root" -} diff --git a/.fernignore b/.fernignore new file mode 100644 index 0000000..d7d43bf --- /dev/null +++ b/.fernignore @@ -0,0 +1,8 @@ +# Specify files that shouldn't be modified by Fern +.gitignore +.vscode/ + +src/browser_use/client.py +src/browser_use/lib/ +src/browser_use/wrapper/ +examples/ \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ccb20d4..66780cf 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,98 +1,37 @@ -name: CI -on: - push: - branches-ignore: - - 'generated' - - 'codegen/**' - - 'integrated/**' - - 'stl-preview-head/**' - - 'stl-preview-base/**' - pull_request: - branches-ignore: - - 'stl-preview-head/**' - - 'stl-preview-base/**' +name: ci +on: [push] jobs: - lint: - timeout-minutes: 10 - name: lint - runs-on: ${{ github.repository == 'stainless-sdks/browser-use-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} - if: github.event_name == 'push' || github.event.pull_request.head.repo.fork + compile: + runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - - name: Install Rye - run: | - curl -sSf https://rye.astral.sh/get | bash - echo "$HOME/.rye/shims" >> $GITHUB_PATH - env: - RYE_VERSION: '0.44.0' - RYE_INSTALL_OPTION: '--yes' - - - name: Install dependencies - run: rye sync --all-features - - - name: Run lints - run: ./scripts/lint - - build: - if: github.event_name == 'push' || github.event.pull_request.head.repo.fork - timeout-minutes: 10 - name: build - permissions: - contents: read - id-token: write - runs-on: depot-ubuntu-24.04 - steps: - - uses: actions/checkout@v4 - - - name: Install Rye + - name: Checkout repo + uses: actions/checkout@v4 + - name: Set up python + uses: actions/setup-python@v4 + with: + python-version: 3.8 + - name: Bootstrap poetry 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' - + curl -sSL https://install.python-poetry.org | python - -y --version 1.5.1 - name: Install dependencies - run: rye sync --all-features - - - name: Run build - run: rye build - - - name: Get GitHub OIDC Token - if: github.repository == 'stainless-sdks/browser-use-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/browser-use-python' - env: - URL: https://pkg.stainless.com/s - AUTH: ${{ steps.github-oidc.outputs.github_token }} - SHA: ${{ github.sha }} - run: ./scripts/utils/upload-artifact.sh - + run: poetry install + - name: Compile + run: poetry run mypy . test: - timeout-minutes: 10 - name: test - runs-on: ${{ github.repository == 'stainless-sdks/browser-use-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} - if: github.event_name == 'push' || github.event.pull_request.head.repo.fork + runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - - name: Install Rye + - name: Checkout repo + uses: actions/checkout@v4 + - name: Set up python + uses: actions/setup-python@v4 + with: + python-version: 3.8 + - name: Bootstrap poetry run: | - curl -sSf https://rye.astral.sh/get | bash - echo "$HOME/.rye/shims" >> $GITHUB_PATH - env: - RYE_VERSION: '0.44.0' - RYE_INSTALL_OPTION: '--yes' - - - name: Bootstrap - run: ./scripts/bootstrap + curl -sSL https://install.python-poetry.org | python - -y --version 1.5.1 + - name: Install dependencies + run: poetry install - - name: Run tests - run: ./scripts/test + - name: Test + run: poetry run pytest -rP . diff --git a/.github/workflows/publish-pypi.yml b/.github/workflows/publish-pypi.yml deleted file mode 100644 index d917646..0000000 --- a/.github/workflows/publish-pypi.yml +++ /dev/null @@ -1,31 +0,0 @@ -# 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/browser-use/browser-use-python/actions/workflows/publish-pypi.yml -name: Publish PyPI -on: - workflow_dispatch: - - release: - types: [published] - -jobs: - publish: - name: publish - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v4 - - - name: Install Rye - run: | - curl -sSf https://rye.astral.sh/get | bash - echo "$HOME/.rye/shims" >> $GITHUB_PATH - env: - RYE_VERSION: '0.44.0' - RYE_INSTALL_OPTION: '--yes' - - - name: Publish to PyPI - run: | - bash ./bin/publish-pypi - env: - PYPI_TOKEN: ${{ secrets.BROWSER_USE_PYPI_TOKEN || secrets.PYPI_TOKEN }} diff --git a/.github/workflows/release-doctor.yml b/.github/workflows/release-doctor.yml deleted file mode 100644 index 8d69a5b..0000000 --- a/.github/workflows/release-doctor.yml +++ /dev/null @@ -1,21 +0,0 @@ -name: Release Doctor -on: - pull_request: - branches: - - main - workflow_dispatch: - -jobs: - release_doctor: - name: release doctor - runs-on: ubuntu-latest - if: github.repository == 'browser-use/browser-use-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.BROWSER_USE_PYPI_TOKEN || secrets.PYPI_TOKEN }} diff --git a/.gitignore b/.gitignore index 95ceb18..7979467 100644 --- a/.gitignore +++ b/.gitignore @@ -1,15 +1,8 @@ -.prism.log -_dev - -__pycache__ -.mypy_cache - -dist - -.venv -.idea - -.env -.envrc -codegen.log -Brewfile.lock.json +.mypy_cache/ +.ruff_cache/ +__pycache__/ +dist/ +poetry.toml + +.env* +!.env.example \ No newline at end of file diff --git a/.python-version b/.python-version deleted file mode 100644 index 43077b2..0000000 --- a/.python-version +++ /dev/null @@ -1 +0,0 @@ -3.9.18 diff --git a/.release-please-manifest.json b/.release-please-manifest.json deleted file mode 100644 index 06d6df2..0000000 --- a/.release-please-manifest.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - ".": "1.0.2" -} \ No newline at end of file diff --git a/.stats.yml b/.stats.yml deleted file mode 100644 index 808f944..0000000 --- a/.stats.yml +++ /dev/null @@ -1,4 +0,0 @@ -configured_endpoints: 26 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browser-use%2Fbrowser-use-814bdd9f98b750d42a2b713a0a12b14fc5a0241ff820b2fbc7666ab2e9a5443f.yml -openapi_spec_hash: 0dae4d4d33a3ec93e470f9546e43fad3 -config_hash: dd3e22b635fa0eb9a7c741a8aaca2a7f diff --git a/.vscode/settings.json b/.vscode/settings.json index 5b01030..63139ca 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,3 +1,7 @@ { - "python.analysis.importFormat": "relative", + "python.analysis.importFormat": "relative", + "editor.codeActionsOnSave": { + "source.organizeImports": "always", + "source.fixAll.ruff": "always" + } } diff --git a/Brewfile b/Brewfile deleted file mode 100644 index 492ca37..0000000 --- a/Brewfile +++ /dev/null @@ -1,2 +0,0 @@ -brew "rye" - diff --git a/CHANGELOG.md b/CHANGELOG.md deleted file mode 100644 index 284fac7..0000000 --- a/CHANGELOG.md +++ /dev/null @@ -1,68 +0,0 @@ -# Changelog - -## 1.0.2 (2025-08-22) - -Full Changelog: [v1.0.1...v1.0.2](https://github.com/browser-use/browser-use-python/compare/v1.0.1...v1.0.2) - -### Chores - -* update github action ([655a660](https://github.com/browser-use/browser-use-python/commit/655a6600972aee2cefcf83c73fdfd9e1ae68b852)) - -## 1.0.1 (2025-08-21) - -Full Changelog: [v1.0.0...v1.0.1](https://github.com/browser-use/browser-use-python/compare/v1.0.0...v1.0.1) - -### Bug Fixes - -* Improve Quick Start Section ([c994ab2](https://github.com/browser-use/browser-use-python/commit/c994ab27ce570d95961f84d8e54a48dd04bd3dfc)) - -## 1.0.0 (2025-08-20) - -Full Changelog: [v0.3.0...v1.0.0](https://github.com/browser-use/browser-use-python/compare/v0.3.0...v1.0.0) - -## 0.3.0 (2025-08-20) - -Full Changelog: [v0.2.0...v0.3.0](https://github.com/browser-use/browser-use-python/compare/v0.2.0...v0.3.0) - -### Features - -* LLM key strings over LLM model enum ([0f5930a](https://github.com/browser-use/browser-use-python/commit/0f5930a7760190523f1d3e969c66e0a34ff075b3)) - -## 0.2.0 (2025-08-19) - -Full Changelog: [v0.1.0...v0.2.0](https://github.com/browser-use/browser-use-python/compare/v0.1.0...v0.2.0) - -### Features - -* **api:** manual updates ([6266282](https://github.com/browser-use/browser-use-python/commit/6266282a615344fdab0737d29adc9124a3bf8b8d)) -* **api:** manual updates ([2d9ba52](https://github.com/browser-use/browser-use-python/commit/2d9ba52b23e53c581360afc655fa8d665a106814)) -* Improve Docs ([6e79b7c](https://github.com/browser-use/browser-use-python/commit/6e79b7c5cfc7cf54f1474521025fa713f200bc3b)) - -## 0.1.0 (2025-08-18) - -Full Changelog: [v0.0.2...v0.1.0](https://github.com/browser-use/browser-use-python/compare/v0.0.2...v0.1.0) - -### Features - -* Add start_url ([2ede0a9](https://github.com/browser-use/browser-use-python/commit/2ede0a9089bfbba1eca207508a52ee36b4ef18ac)) -* Align Task Filtering by Status with `status` Field ([29b4590](https://github.com/browser-use/browser-use-python/commit/29b4590c69f13fbf7f855888862ef77a9e704172)) -* **api:** api update ([5867532](https://github.com/browser-use/browser-use-python/commit/58675327b6a0e7ba41f312e4887062a9b6dc2852)) -* **api:** manual updates ([78727c0](https://github.com/browser-use/browser-use-python/commit/78727c02cefa53fd0dd877e137b7b6f92e14fce8)) -* **api:** update via SDK Studio ([b283386](https://github.com/browser-use/browser-use-python/commit/b283386b805435a87114e807f8919185cb6a5b7b)) -* Fix Stainless GitHub Action ([5dcf360](https://github.com/browser-use/browser-use-python/commit/5dcf360ccfe40f45962ecaa64b8a5aacf55778d4)) -* Update param and response views ([44b4c5d](https://github.com/browser-use/browser-use-python/commit/44b4c5d7ed416f9f5c37afb3287cdaa6f22a30cd)) - - -### Chores - -* **internal:** codegen related update ([151d56b](https://github.com/browser-use/browser-use-python/commit/151d56ba67c2d09970ff415472c0a1d259716bbc)) - -## 0.0.2 (2025-08-09) - -Full Changelog: [v0.0.1...v0.0.2](https://github.com/browser-use/browser-use-python/compare/v0.0.1...v0.0.2) - -### Chores - -* configure new SDK language ([af51d4f](https://github.com/browser-use/browser-use-python/commit/af51d4f1d2ff224d0a2cba426b28d540d74f63ce)) -* update SDK settings ([4fcafb0](https://github.com/browser-use/browser-use-python/commit/4fcafb0a1cbd6fda1c28c0996fe3de4eb033b107)) -* update SDK settings ([20019d1](https://github.com/browser-use/browser-use-python/commit/20019d1ec80d3c75dfb7ca54131b66e9dc0dd542)) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md deleted file mode 100644 index 5338abc..0000000 --- a/CONTRIBUTING.md +++ /dev/null @@ -1,128 +0,0 @@ -## Setting up the environment - -### With Rye - -We use [Rye](https://rye.astral.sh/) to manage dependencies because it will automatically provision a Python environment with the expected Python version. To set it up, run: - -```sh -$ ./scripts/bootstrap -``` - -Or [install Rye manually](https://rye.astral.sh/guide/installation/) and run: - -```sh -$ rye sync --all-features -``` - -You can then run scripts using `rye run python script.py` or by activating the virtual environment: - -```sh -# Activate the virtual environment - https://docs.python.org/3/library/venv.html#how-venvs-work -$ source .venv/bin/activate - -# now you can omit the `rye run` prefix -$ python script.py -``` - -### Without Rye - -Alternatively if you don't want to install `Rye`, you can stick with the standard `pip` setup by ensuring you have the Python version specified in `.python-version`, create a virtual environment however you desire and then install dependencies using this command: - -```sh -$ pip install -r requirements-dev.lock -``` - -## Modifying/Adding code - -Most of the SDK is generated code. Modifications to code will be persisted between generations, but may -result in merge conflicts between manual patches and changes from the generator. The generator will never -modify the contents of the `src/browser_use_sdk/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/browser-use/browser-use-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/browser-use/browser-use-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 deleted file mode 100644 index 6eff678..0000000 --- a/LICENSE +++ /dev/null @@ -1,201 +0,0 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright 2025 Browser Use - - 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 79ee251..c23c96c 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,9 @@ -Browser Use Python +# BrowserUse Python Library -[![PyPI version]()](https://pypi.org/project/browser-use-sdk/) +[![fern shield](https://img.shields.io/badge/%F0%9F%8C%BF-Built%20with%20Fern-brightgreen)](https://buildwithfern.com?utm_source=github&utm_medium=github&utm_campaign=readme&utm_source=https%3A%2F%2Fgithub.com%2Fbrowser-use%2Fbrowser-use-python) +[![pypi](https://img.shields.io/pypi/v/browser-use)](https://pypi.python.org/pypi/browser-use) -```sh -pip install browser-use-sdk -``` +The BrowserUse Python library provides convenient access to the BrowserUse APIs from Python. ## Two-Step QuickStart @@ -17,11 +16,14 @@ from browser_use_sdk import BrowserUse client = BrowserUse(api_key="bu_...") -result = client.tasks.run( +task = client.tasks.create_task( task="Search for the top 10 Hacker News posts and return the title and url." + llm="gpt-4.1", ) -result.done_output +result = task.complete() + +result.output ``` > The full API of this library can be found in [api.md](api.md). @@ -39,16 +41,18 @@ class SearchResult(BaseModel): posts: List[HackerNewsPost] async def main() -> None: - result = await client.tasks.run( + task = await client.tasks.create_task( task=""" Find top 10 Hacker News articles and return the title and url. """, - structured_output_json=SearchResult, + schema=SearchResult, ) - if structured_result.parsed_output is not None: + result = await task.complete() + + if result.parsed_output is not None: print("Top HackerNews Posts:") - for post in structured_result.parsed_output.posts: + for post in result.parsed_output.posts: print(f" - {post.title} - {post.url}") asyncio.run(main()) @@ -74,25 +78,18 @@ async def main() -> None: task=""" Find top 10 Hacker News articles and return the title and url. """, - structured_output_json=SearchResult, + schema=SearchResult, ) - async for update in client.tasks.stream(task.id, structured_output_json=SearchResult): - if len(update.steps) > 0: - last_step = update.steps[-1] - print(f"{update.status}: {last_step.url} ({last_step.next_goal})") - else: - print(f"{update.status}") + async for step in task.stream(): + print(f"Step {step.number}: {step.url} ({step.next_goal})") - if update.status == "finished": - if update.parsed_output is None: - print("No output...") - else: - print("Top HackerNews Posts:") - for post in update.parsed_output.posts: - print(f" - {post.title} - {post.url}") + result = await task.complete() - break + if result.parsed_output is not None: + print("Top HackerNews Posts:") + for post in result.parsed_output.posts: + print(f" - {post.title} - {post.url}") asyncio.run(main()) ``` @@ -106,7 +103,7 @@ Browser Use SDK lets you easily verify the signature and structure of the payloa ```py import uvicorn import os -from browser_use_sdk.lib.webhooks import Webhook, verify_webhook_event_signature +from browser_use_sdk import Webhook, verify_webhook_event_signature from fastapi import FastAPI, Request, HTTPException @@ -154,10 +151,11 @@ client = AsyncBrowserUse( async def main() -> None: - task = await client.tasks.run( + task = await client.tasks.create_task( task="Search for the top 10 Hacker News posts and return the title and url.", ) - print(task.done_output) + + print(task.id) asyncio.run(main()) @@ -200,6 +198,80 @@ asyncio.run(main()) ## Advanced +### Access Raw Response Data + +The SDK provides access to raw response data, including headers, through the `.with_raw_response` property. +The `.with_raw_response` property returns a "raw" client that can be used to access the `.headers` and `.data` attributes. + +```python +from browser_use import BrowserUse + +client = BrowserUse( + ..., +) +response = client.tasks.with_raw_response.create_task(...) +print(response.headers) # access the response headers +print(response.data) # access the underlying object +``` + +### Retries + +The SDK is instrumented with automatic retries with exponential backoff. A request will be retried as long +as the request is deemed retryable and the number of retry attempts has not grown larger than the configured +retry limit (default: 2). + +A request is deemed retryable when any of the following HTTP status codes is returned: + +- [408](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/408) (Timeout) +- [429](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/429) (Too Many Requests) +- [5XX](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/500) (Internal Server Errors) + +Use the `max_retries` request option to configure this behavior. + +```python +client.tasks.create_task(..., request_options={ + "max_retries": 1 +}) +``` + +### Timeouts + +The SDK defaults to a 60 second timeout. You can configure this with a timeout option at the client or request level. + +```python + +from browser_use import BrowserUse + +client = BrowserUse( + ..., + timeout=20.0, +) + + +# Override timeout for a specific method +client.tasks.create_task(..., request_options={ + "timeout_in_seconds": 1 +}) +``` + +### Custom Client + +You can override the `httpx` client to customize it for your use-case. Some common use-cases include support for proxies +and transports. + +```python +import httpx +from browser_use import BrowserUse + +client = BrowserUse( + ..., + httpx_client=httpx.Client( + proxies="http://my.test.proxy.example.com", + transport=httpx.HTTPTransport(local_address="0.0.0.0"), + ), +) +``` + ## 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 `browser_use_sdk.APIConnectionError` is raised. @@ -388,4 +460,73 @@ Python 3.8 or higher. ## Contributing -See [the contributing documentation](./CONTRIBUTING.md). +While we value open-source contributions to this SDK, this library is generated programmatically. +Additions made directly to this library would have to be moved over to our generation code, +otherwise they would be overwritten upon the next generated release. Feel free to open a PR as +a proof of concept, but know that we will not be able to merge it as-is. We suggest opening +an issue first to discuss with us! + +On the other hand, contributions to the README are always very welcome! + +## Installation + +```sh +pip install browser-use +``` + +## Reference + +A full reference for this library is available [here](https://github.com/browser-use/browser-use-python/blob/HEAD/./reference.md). + +## Usage + +Instantiate and use the client with the following: + +```python +from browser_use import BrowserUse + +client = BrowserUse( + api_key="YOUR_API_KEY", +) +client.tasks.create_task( + task="task", +) +``` + +## Async Client + +The SDK also exports an `async` client so that you can make non-blocking calls to our API. + +```python +import asyncio + +from browser_use import AsyncBrowserUse + +client = AsyncBrowserUse( + api_key="YOUR_API_KEY", +) + + +async def main() -> None: + await client.tasks.create_task( + task="task", + ) + + +asyncio.run(main()) +``` + +## Exception Handling + +When the API returns a non-success status code (4xx or 5xx response), a subclass of the following error +will be thrown. + +```python +from browser_use.core.api_error import ApiError + +try: + client.tasks.create_task(...) +except ApiError as e: + print(e.status_code) + print(e.body) +``` diff --git a/SECURITY.md b/SECURITY.md deleted file mode 100644 index fa6b52c..0000000 --- a/SECURITY.md +++ /dev/null @@ -1,27 +0,0 @@ -# Security Policy - -## Reporting Security Issues - -This SDK is generated by [Stainless Software Inc](http://stainless.com). Stainless takes security seriously, and encourages you to report any security vulnerability promptly so that appropriate action can be taken. - -To report a security issue, please contact the Stainless team at security@stainless.com. - -## Responsible Disclosure - -We appreciate the efforts of security researchers and individuals who help us maintain the security of -SDKs we generate. If you believe you have found a security vulnerability, please adhere to responsible -disclosure practices by allowing us a reasonable amount of time to investigate and address the issue -before making any information public. - -## Reporting Non-SDK Related Security Issues - -If you encounter security issues that are not directly related to SDKs but pertain to the services -or products provided by Browser Use, please follow the respective company's security reporting guidelines. - -### Browser Use Terms and Policies - -Please contact support@browser-use.com for any questions or concerns regarding the 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 deleted file mode 100644 index 9c56e23..0000000 --- a/api.md +++ /dev/null @@ -1,115 +0,0 @@ -# Users - -## Me - -Types: - -```python -from browser_use_sdk.types.users import MeRetrieveResponse -``` - -Methods: - -- client.users.me.retrieve() -> MeRetrieveResponse - -### Files - -Types: - -```python -from browser_use_sdk.types.users.me import FileCreatePresignedURLResponse -``` - -Methods: - -- client.users.me.files.create_presigned_url(\*\*params) -> FileCreatePresignedURLResponse - -# Tasks - -Types: - -```python -from browser_use_sdk.types import ( - FileView, - TaskItemView, - TaskStatus, - TaskStepView, - TaskView, - TaskCreateResponse, - TaskListResponse, - TaskGetLogsResponse, - TaskGetOutputFileResponse, - TaskGetUserUploadedFileResponse, -) -``` - -Methods: - -- client.tasks.create(\*\*params) -> TaskCreateResponse -- client.tasks.retrieve(task_id) -> TaskView -- client.tasks.update(task_id, \*\*params) -> TaskView -- client.tasks.list(\*\*params) -> TaskListResponse -- client.tasks.get_logs(task_id) -> TaskGetLogsResponse -- client.tasks.get_output_file(file_id, \*, task_id) -> TaskGetOutputFileResponse -- client.tasks.get_user_uploaded_file(file_id, \*, task_id) -> TaskGetUserUploadedFileResponse - -# Sessions - -Types: - -```python -from browser_use_sdk.types import SessionStatus, SessionView, SessionListResponse -``` - -Methods: - -- client.sessions.retrieve(session_id) -> SessionView -- client.sessions.update(session_id, \*\*params) -> SessionView -- client.sessions.list(\*\*params) -> SessionListResponse -- client.sessions.delete(session_id) -> None - -## PublicShare - -Types: - -```python -from browser_use_sdk.types.sessions import ShareView -``` - -Methods: - -- client.sessions.public_share.create(session_id) -> ShareView -- client.sessions.public_share.retrieve(session_id) -> ShareView -- client.sessions.public_share.delete(session_id) -> None - -# BrowserProfiles - -Types: - -```python -from browser_use_sdk.types import BrowserProfileView, ProxyCountryCode, BrowserProfileListResponse -``` - -Methods: - -- client.browser_profiles.create(\*\*params) -> BrowserProfileView -- client.browser_profiles.retrieve(profile_id) -> BrowserProfileView -- client.browser_profiles.update(profile_id, \*\*params) -> BrowserProfileView -- client.browser_profiles.list(\*\*params) -> BrowserProfileListResponse -- client.browser_profiles.delete(profile_id) -> None - -# AgentProfiles - -Types: - -```python -from browser_use_sdk.types import AgentProfileView, AgentProfileListResponse -``` - -Methods: - -- client.agent_profiles.create(\*\*params) -> AgentProfileView -- client.agent_profiles.retrieve(profile_id) -> AgentProfileView -- client.agent_profiles.update(profile_id, \*\*params) -> AgentProfileView -- client.agent_profiles.list(\*\*params) -> AgentProfileListResponse -- client.agent_profiles.delete(profile_id) -> None diff --git a/assets/cloud-banner-python.png b/assets/cloud-banner-python.png deleted file mode 100644 index 77e2aee..0000000 Binary files a/assets/cloud-banner-python.png and /dev/null differ diff --git a/bin/check-release-environment b/bin/check-release-environment deleted file mode 100644 index b845b0f..0000000 --- a/bin/check-release-environment +++ /dev/null @@ -1,21 +0,0 @@ -#!/usr/bin/env bash - -errors=() - -if [ -z "${PYPI_TOKEN}" ]; then - errors+=("The PYPI_TOKEN secret has not been set. Please set it in either this repository's secrets or your organization secrets.") -fi - -lenErrors=${#errors[@]} - -if [[ lenErrors -gt 0 ]]; then - echo -e "Found the following errors in the release environment:\n" - - for error in "${errors[@]}"; do - echo -e "- $error\n" - done - - exit 1 -fi - -echo "The environment is ready to push releases!" diff --git a/bin/publish-pypi b/bin/publish-pypi deleted file mode 100644 index 826054e..0000000 --- a/bin/publish-pypi +++ /dev/null @@ -1,6 +0,0 @@ -#!/usr/bin/env bash - -set -eux -mkdir -p dist -rye build --clean -rye publish --yes --token=$PYPI_TOKEN diff --git a/examples/.keep b/examples/.keep deleted file mode 100644 index d8c73e9..0000000 --- a/examples/.keep +++ /dev/null @@ -1,4 +0,0 @@ -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/examples/api.py b/examples/api.py new file mode 100644 index 0000000..53756c9 --- /dev/null +++ b/examples/api.py @@ -0,0 +1,7 @@ +import os + +_API_KEY = os.getenv("BROWSER_USE_API_KEY") +if not _API_KEY: + raise RuntimeError("BROWSER_USE_API_KEY environment variable is not set") + +API_KEY = _API_KEY diff --git a/examples/async_create.py b/examples/async_create.py deleted file mode 100755 index 9cfac77..0000000 --- a/examples/async_create.py +++ /dev/null @@ -1,57 +0,0 @@ -#!/usr/bin/env -S rye run python - -import asyncio -from typing import List - -from pydantic import BaseModel - -from browser_use_sdk import AsyncBrowserUse - -# gets API Key from environment variable BROWSER_USE_API_KEY -client = AsyncBrowserUse() - - -# Regular Task -async def create_regular_task() -> None: - res = await client.tasks.create( - task=""" - Find top 10 Hacker News articles and return the title and url. - """, - agent_settings={"llm": "gemini-2.5-flash"}, - ) - - print(f"Regular Task ID: {res.id}") - - -# Structured Output -async def create_structured_task() -> None: - class HackerNewsPost(BaseModel): - title: str - url: str - - class SearchResult(BaseModel): - posts: List[HackerNewsPost] - - res = await client.tasks.create( - task=""" - Find top 10 Hacker News articles and return the title and url. - """, - agent_settings={"llm": "gpt-4.1"}, - structured_output_json=SearchResult, - ) - - print(f"Structured Task ID: {res.id}") - - -# Main - - -async def main() -> None: - await asyncio.gather( - # - create_regular_task(), - create_structured_task(), - ) - - -asyncio.run(main()) diff --git a/examples/async_retrieve.py b/examples/async_retrieve.py index 1868e7e..82f19b7 100755 --- a/examples/async_retrieve.py +++ b/examples/async_retrieve.py @@ -1,14 +1,14 @@ -#!/usr/bin/env -S rye run python +#!/usr/bin/env -S poetry run python import asyncio from typing import List +from api import API_KEY from pydantic import BaseModel -from browser_use_sdk import AsyncBrowserUse +from browser_use import AsyncBrowserUse -# gets API Key from environment variable BROWSER_USE_API_KEY -client = AsyncBrowserUse() +client = AsyncBrowserUse(api_key=API_KEY) # Regular Task @@ -19,20 +19,20 @@ async def retrieve_regular_task() -> None: print("Retrieving regular task...") - regular_task = await client.tasks.create( + task = await client.tasks.create_task( task=""" Find top 10 Hacker News articles and return the title and url. """, - agent_settings={"llm": "gemini-2.5-flash"}, + llm="gemini-2.5-flash", ) - print(f"Regular Task ID: {regular_task.id}") + print(f"Regular Task ID: {task.id}") while True: - regular_status = await client.tasks.retrieve(regular_task.id) + regular_status = await client.tasks.get_task(task.id) print(f"Regular Task Status: {regular_status.status}") if regular_status.status == "finished": - print(f"Regular Task Output: {regular_status.done_output}") + print(f"Regular Task Output: {regular_status.output}") break await asyncio.sleep(1) @@ -55,18 +55,18 @@ class HackerNewsPost(BaseModel): class SearchResult(BaseModel): posts: List[HackerNewsPost] - structured_task = await client.tasks.create( + task = await client.tasks.create_task( task=""" Find top 10 Hacker News articles and return the title and url. """, - agent_settings={"llm": "gpt-4.1"}, - structured_output_json=SearchResult, + llm="gpt-4.1", + schema=SearchResult, ) - print(f"Structured Task ID: {structured_task.id}") + print(f"Structured Task ID: {task.id}") while True: - structured_status = await client.tasks.retrieve(task_id=structured_task.id, structured_output_json=SearchResult) + structured_status = await client.tasks.get_task(task_id=task.id, schema=SearchResult) print(f"Structured Task Status: {structured_status.status}") if structured_status.status == "finished": diff --git a/examples/async_run.py b/examples/async_run.py index ecb1d11..e0b6c9a 100755 --- a/examples/async_run.py +++ b/examples/async_run.py @@ -1,28 +1,30 @@ -#!/usr/bin/env -S rye run python +#!/usr/bin/env -S poetry run python import asyncio from typing import List +from api import API_KEY from pydantic import BaseModel -from browser_use_sdk import AsyncBrowserUse +from browser_use import AsyncBrowserUse -# gets API Key from environment variable BROWSER_USE_API_KEY -client = AsyncBrowserUse() +client = AsyncBrowserUse(api_key=API_KEY) # Regular Task async def run_regular_task() -> None: - regular_result = await client.tasks.run( + task = await client.tasks.create_task( task=""" Find top 10 Hacker News articles and return the title and url. """, - agent_settings={"llm": "gemini-2.5-flash"}, + llm="gemini-2.5-flash", ) - print(f"Regular Task ID: {regular_result.id}") + print(f"Regular Task ID: {task.id}") - print(f"Regular Task Output: {regular_result.done_output}") + result = await task.complete() + + print(f"Regular Task Output: {result.output}") print("Done") @@ -36,19 +38,21 @@ class HackerNewsPost(BaseModel): class SearchResult(BaseModel): posts: List[HackerNewsPost] - structured_result = await client.tasks.run( + task = await client.tasks.create_task( task=""" Find top 10 Hacker News articles and return the title and url. """, - agent_settings={"llm": "gpt-4.1"}, - structured_output_json=SearchResult, + llm="gpt-4.1", + schema=SearchResult, ) - print(f"Structured Task ID: {structured_result.id}") + print(f"Structured Task ID: {task.id}") + + result = await task.complete() - if structured_result.parsed_output is not None: + if result.parsed_output is not None: print("Structured Task Output:") - for post in structured_result.parsed_output.posts: + for post in result.parsed_output.posts: print(f" - {post.title} - {post.url}") print("Structured Task Done") diff --git a/examples/async_stream.py b/examples/async_stream.py index 772b781..244e792 100755 --- a/examples/async_stream.py +++ b/examples/async_stream.py @@ -1,36 +1,34 @@ -#!/usr/bin/env -S rye run python +#!/usr/bin/env -S poetry run python import asyncio from typing import List +from api import API_KEY from pydantic import BaseModel -from browser_use_sdk import AsyncBrowserUse -from browser_use_sdk.types.task_create_params import AgentSettings +from browser_use import AsyncBrowserUse -# gets API Key from environment variable BROWSER_USE_API_KEY -client = AsyncBrowserUse() +client = AsyncBrowserUse(api_key=API_KEY) # Regular Task async def stream_regular_task() -> None: - regular_task = await client.tasks.create( + task = await client.tasks.create_task( task=""" Find top 10 Hacker News articles and return the title and url. """, - agent_settings=AgentSettings(llm="gemini-2.5-flash"), + llm="gemini-2.5-flash", ) - print(f"Regular Task ID: {regular_task.id}") + print(f"Regular Task ID: {task.id}") - async for res in client.tasks.stream(regular_task.id): - print(f"Regular Task Status: {res.status}") + async for step in task.stream(): + print(f"Regular Task Status: {step.number}") - if len(res.steps) > 0: - last_step = res.steps[-1] - print(f"Regular Task Step: {last_step.url} ({last_step.next_goal})") - for action in last_step.actions: - print(f" - Regular Task Action: {action}") + result = await task.complete() + + if result.output is not None: + print(f"Regular Task Output: {result.output}") print("Regular Task Done") @@ -44,26 +42,26 @@ class HackerNewsPost(BaseModel): class SearchResult(BaseModel): posts: List[HackerNewsPost] - structured_task = await client.tasks.create( + task = await client.tasks.create_task( task=""" Find top 10 Hacker News articles and return the title and url. """, - agent_settings={"llm": "gpt-4.1"}, - structured_output_json=SearchResult, + llm="gpt-4.1", + schema=SearchResult, ) - print(f"Structured Task ID: {structured_task.id}") + print(f"Structured Task ID: {task.id}") + + async for step in task.stream(): + print(f"Structured Task Step {step.number}: {step.url} ({step.next_goal})") + + result = await task.complete() - async for res in client.tasks.stream(structured_task.id, structured_output_json=SearchResult): - print(f"Structured Task Status: {res.status}") + if result.parsed_output is not None: + print("Structured Task Output:") - if res.status == "finished": - if res.parsed_output is None: - print("Structured Task No output") - else: - for post in res.parsed_output.posts: - print(f" - Structured Task Post: {post.title} - {post.url}") - break + for post in result.parsed_output.posts: + print(f" - {post.title} - {post.url}") print("Structured Task Done") diff --git a/examples/create.py b/examples/create.py deleted file mode 100755 index b0af2db..0000000 --- a/examples/create.py +++ /dev/null @@ -1,48 +0,0 @@ -#!/usr/bin/env -S rye run python - -from typing import List - -from pydantic import BaseModel - -from browser_use_sdk import BrowserUse - -# gets API Key from environment variable BROWSER_USE_API_KEY -client = BrowserUse() - - -# Regular Task -def create_regular_task() -> None: - res = client.tasks.create( - task=""" - Find top 10 Hacker News articles and return the title and url. - """, - agent_settings={"llm": "gemini-2.5-flash"}, - ) - - print(res.id) - - -create_regular_task() - - -# Structured Output -def create_structured_task() -> None: - class HackerNewsPost(BaseModel): - title: str - url: str - - class SearchResult(BaseModel): - posts: List[HackerNewsPost] - - res = client.tasks.create( - task=""" - Find top 10 Hacker News articles and return the title and url. - """, - agent_settings={"llm": "gpt-4.1"}, - structured_output_json=SearchResult, - ) - - print(res.id) - - -create_structured_task() diff --git a/examples/retrieve.py b/examples/retrieve.py index e2c4e47..121adbe 100755 --- a/examples/retrieve.py +++ b/examples/retrieve.py @@ -1,14 +1,14 @@ -#!/usr/bin/env -S rye run python +#!/usr/bin/env -S poetry run python import time from typing import List +from api import API_KEY from pydantic import BaseModel -from browser_use_sdk import BrowserUse +from browser_use import BrowserUse -# gets API Key from environment variable BROWSER_USE_API_KEY -client = BrowserUse() +client = BrowserUse(api_key=API_KEY) # Regular Task @@ -19,20 +19,20 @@ def retrieve_regular_task() -> None: print("Retrieving regular task...") - regular_task = client.tasks.create( + task = client.tasks.create_task( task=""" Find top 10 Hacker News articles and return the title and url. """, - agent_settings={"llm": "gemini-2.5-flash"}, + llm="gemini-2.5-flash", ) - print(f"Task ID: {regular_task.id}") + print(f"Task ID: {task.id}") while True: - regular_status = client.tasks.retrieve(regular_task.id) + regular_status = client.tasks.get_task(task.id) print(regular_status.status) if regular_status.status == "finished": - print(regular_status.done_output) + print(regular_status.output) break time.sleep(1) @@ -58,18 +58,18 @@ class HackerNewsPost(BaseModel): class SearchResult(BaseModel): posts: List[HackerNewsPost] - structured_task = client.tasks.create( + task = client.tasks.create_task( task=""" Find top 10 Hacker News articles and return the title and url. """, - agent_settings={"llm": "gpt-4.1"}, - structured_output_json=SearchResult, + llm="gpt-4.1", + schema=SearchResult, ) - print(f"Task ID: {structured_task.id}") + print(f"Task ID: {task.id}") while True: - structured_status = client.tasks.retrieve(task_id=structured_task.id, structured_output_json=SearchResult) + structured_status = client.tasks.get_task(task_id=task.id, schema=SearchResult) print(structured_status.status) if structured_status.status == "finished": diff --git a/examples/run.py b/examples/run.py index 14b0c00..8f4fc50 100755 --- a/examples/run.py +++ b/examples/run.py @@ -1,27 +1,29 @@ -#!/usr/bin/env -S rye run python +#!/usr/bin/env -S poetry run python from typing import List +from api import API_KEY from pydantic import BaseModel -from browser_use_sdk import BrowserUse +from browser_use import BrowserUse -# gets API Key from environment variable BROWSER_USE_API_KEY -client = BrowserUse() +client = BrowserUse(api_key=API_KEY) # Regular Task def run_regular_task() -> None: - regular_result = client.tasks.run( + task = client.tasks.create_task( task=""" Find top 10 Hacker News articles and return the title and url. """, - agent_settings={"llm": "gemini-2.5-flash"}, + llm="gemini-2.5-flash", ) - print(f"Task ID: {regular_result.id}") + print(f"Task ID: {task.id}") - print(regular_result.done_output) + result = task.complete() + + print(result.output) print("Done") @@ -38,18 +40,20 @@ class HackerNewsPost(BaseModel): class SearchResult(BaseModel): posts: List[HackerNewsPost] - structured_result = client.tasks.run( + task = client.tasks.create_task( task=""" Find top 10 Hacker News articles and return the title and url. """, - agent_settings={"llm": "gpt-4.1"}, - structured_output_json=SearchResult, + llm="gpt-4.1", + schema=SearchResult, ) - print(f"Task ID: {structured_result.id}") + print(f"Task ID: {task.id}") + + result = task.complete() - if structured_result.parsed_output is not None: - for post in structured_result.parsed_output.posts: + if result.parsed_output is not None: + for post in result.parsed_output.posts: print(f" - {post.title} - {post.url}") print("Done") diff --git a/examples/stream.py b/examples/stream.py index e017260..e90ad66 100755 --- a/examples/stream.py +++ b/examples/stream.py @@ -1,38 +1,28 @@ -#!/usr/bin/env -S rye run python +#!/usr/bin/env -S poetry run python from typing import List +from api import API_KEY from pydantic import BaseModel -from browser_use_sdk import BrowserUse -from browser_use_sdk.types.task_create_params import AgentSettings +from browser_use import BrowserUse -# gets API Key from environment variable BROWSER_USE_API_KEY -client = BrowserUse() +client = BrowserUse(api_key=API_KEY) # Regular Task def stream_regular_task() -> None: - regular_task = client.tasks.create( + task = client.tasks.create_task( task=""" Find top 10 Hacker News articles and return the title and url. """, - agent_settings=AgentSettings(llm="gemini-2.5-flash"), + llm="gemini-2.5-flash", ) - print(f"Task ID: {regular_task.id}") + print(f"Task ID: {task.id}") - for res in client.tasks.stream(regular_task.id): - print(res.status) - - if len(res.steps) > 0: - last_step = res.steps[-1] - print(f"{last_step.url} ({last_step.next_goal})") - for action in last_step.actions: - print(f" - {action}") - - if res.status == "finished": - print(res.done_output) + for step in task.stream(): + print(f"Step {step.number}: {step.url} ({step.next_goal})") print("Regular: DONE") @@ -49,26 +39,24 @@ class HackerNewsPost(BaseModel): class SearchResult(BaseModel): posts: List[HackerNewsPost] - structured_task = client.tasks.create( + task = client.tasks.create_task( task=""" Find top 10 Hacker News articles and return the title and url. """, - agent_settings={"llm": "gpt-4.1"}, - structured_output_json=SearchResult, + llm="gpt-4.1", + schema=SearchResult, ) - print(f"Task ID: {structured_task.id}") + print(f"Task ID: {task.id}") + + for step in task.stream(): + print(f"Step {step.number}: {step.url} ({step.next_goal})") - for res in client.tasks.stream(structured_task.id, structured_output_json=SearchResult): - print(res.status) + result = task.complete() - if res.status == "finished": - if res.parsed_output is None: - print("No output") - else: - for post in res.parsed_output.posts: - print(f" - {post.title} - {post.url}") - break + if result.parsed_output is not None: + for post in result.parsed_output.posts: + print(f" - {post.title} - {post.url}") print("Done") diff --git a/examples/webhooks.py b/examples/webhooks.py index 65cb93b..a26a29c 100755 --- a/examples/webhooks.py +++ b/examples/webhooks.py @@ -1,9 +1,9 @@ -#!/usr/bin/env -S rye run python +#!/usr/bin/env -S poetry run python -from typing import Any, Dict, Tuple from datetime import datetime +from typing import Any, Dict, Tuple -from browser_use_sdk.lib.webhooks import ( +from browser_use.lib.webhooks import ( Webhook, WebhookAgentTaskStatusUpdate, WebhookAgentTaskStatusUpdatePayload, diff --git a/mypy.ini b/mypy.ini deleted file mode 100644 index d128e00..0000000 --- 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/browser_use_sdk/_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/noxfile.py b/noxfile.py deleted file mode 100644 index 53bca7f..0000000 --- a/noxfile.py +++ /dev/null @@ -1,9 +0,0 @@ -import nox - - -@nox.session(reuse_venv=True, name="test-pydantic-v1") -def test_pydantic_v1(session: nox.Session) -> None: - session.install("-r", "requirements-dev.lock") - session.install("pydantic<2") - - session.run("pytest", "--showlocals", "--ignore=tests/functional", *session.posargs) diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..d07b671 --- /dev/null +++ b/poetry.lock @@ -0,0 +1,578 @@ +# This file is automatically @generated by Poetry 2.1.4 and should not be changed by hand. + +[[package]] +name = "annotated-types" +version = "0.7.0" +description = "Reusable constraint types to use with typing.Annotated" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, + {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, +] + +[package.dependencies] +typing-extensions = {version = ">=4.0.0", markers = "python_version < \"3.9\""} + +[[package]] +name = "anyio" +version = "4.5.2" +description = "High level compatibility layer for multiple asynchronous event loop implementations" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "anyio-4.5.2-py3-none-any.whl", hash = "sha256:c011ee36bc1e8ba40e5a81cb9df91925c218fe9b778554e0b56a21e1b5d4716f"}, + {file = "anyio-4.5.2.tar.gz", hash = "sha256:23009af4ed04ce05991845451e11ef02fc7c5ed29179ac9a420e5ad0ac7ddc5b"}, +] + +[package.dependencies] +exceptiongroup = {version = ">=1.0.2", markers = "python_version < \"3.11\""} +idna = ">=2.8" +sniffio = ">=1.1" +typing-extensions = {version = ">=4.1", markers = "python_version < \"3.11\""} + +[package.extras] +doc = ["Sphinx (>=7.4,<8.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] +test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "truststore (>=0.9.1) ; python_version >= \"3.10\"", "uvloop (>=0.21.0b1) ; platform_python_implementation == \"CPython\" and platform_system != \"Windows\""] +trio = ["trio (>=0.26.1)"] + +[[package]] +name = "certifi" +version = "2025.8.3" +description = "Python package for providing Mozilla's CA Bundle." +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "certifi-2025.8.3-py3-none-any.whl", hash = "sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5"}, + {file = "certifi-2025.8.3.tar.gz", hash = "sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407"}, +] + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +groups = ["dev"] +markers = "sys_platform == \"win32\"" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.0" +description = "Backport of PEP 654 (exception groups)" +optional = false +python-versions = ">=3.7" +groups = ["main", "dev"] +markers = "python_version < \"3.11\"" +files = [ + {file = "exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10"}, + {file = "exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88"}, +] + +[package.dependencies] +typing-extensions = {version = ">=4.6.0", markers = "python_version < \"3.13\""} + +[package.extras] +test = ["pytest (>=6)"] + +[[package]] +name = "h11" +version = "0.16.0" +description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86"}, + {file = "h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1"}, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +description = "A minimal low-level HTTP client." +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55"}, + {file = "httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8"}, +] + +[package.dependencies] +certifi = "*" +h11 = ">=0.16" + +[package.extras] +asyncio = ["anyio (>=4.0,<5.0)"] +http2 = ["h2 (>=3,<5)"] +socks = ["socksio (==1.*)"] +trio = ["trio (>=0.22.0,<1.0)"] + +[[package]] +name = "httpx" +version = "0.28.1" +description = "The next generation HTTP client." +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad"}, + {file = "httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc"}, +] + +[package.dependencies] +anyio = "*" +certifi = "*" +httpcore = "==1.*" +idna = "*" + +[package.extras] +brotli = ["brotli ; platform_python_implementation == \"CPython\"", "brotlicffi ; platform_python_implementation != \"CPython\""] +cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] +http2 = ["h2 (>=3,<5)"] +socks = ["socksio (==1.*)"] +zstd = ["zstandard (>=0.18.0)"] + +[[package]] +name = "idna" +version = "3.10" +description = "Internationalized Domain Names in Applications (IDNA)" +optional = false +python-versions = ">=3.6" +groups = ["main"] +files = [ + {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, + {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, +] + +[package.extras] +all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] + +[[package]] +name = "iniconfig" +version = "2.1.0" +description = "brain-dead simple config-ini parsing" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760"}, + {file = "iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7"}, +] + +[[package]] +name = "mypy" +version = "1.13.0" +description = "Optional static typing for Python" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "mypy-1.13.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6607e0f1dd1fb7f0aca14d936d13fd19eba5e17e1cd2a14f808fa5f8f6d8f60a"}, + {file = "mypy-1.13.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8a21be69bd26fa81b1f80a61ee7ab05b076c674d9b18fb56239d72e21d9f4c80"}, + {file = "mypy-1.13.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7b2353a44d2179846a096e25691d54d59904559f4232519d420d64da6828a3a7"}, + {file = "mypy-1.13.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0730d1c6a2739d4511dc4253f8274cdd140c55c32dfb0a4cf8b7a43f40abfa6f"}, + {file = "mypy-1.13.0-cp310-cp310-win_amd64.whl", hash = "sha256:c5fc54dbb712ff5e5a0fca797e6e0aa25726c7e72c6a5850cfd2adbc1eb0a372"}, + {file = "mypy-1.13.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:581665e6f3a8a9078f28d5502f4c334c0c8d802ef55ea0e7276a6e409bc0d82d"}, + {file = "mypy-1.13.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3ddb5b9bf82e05cc9a627e84707b528e5c7caaa1c55c69e175abb15a761cec2d"}, + {file = "mypy-1.13.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:20c7ee0bc0d5a9595c46f38beb04201f2620065a93755704e141fcac9f59db2b"}, + {file = "mypy-1.13.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3790ded76f0b34bc9c8ba4def8f919dd6a46db0f5a6610fb994fe8efdd447f73"}, + {file = "mypy-1.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:51f869f4b6b538229c1d1bcc1dd7d119817206e2bc54e8e374b3dfa202defcca"}, + {file = "mypy-1.13.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5c7051a3461ae84dfb5dd15eff5094640c61c5f22257c8b766794e6dd85e72d5"}, + {file = "mypy-1.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:39bb21c69a5d6342f4ce526e4584bc5c197fd20a60d14a8624d8743fffb9472e"}, + {file = "mypy-1.13.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:164f28cb9d6367439031f4c81e84d3ccaa1e19232d9d05d37cb0bd880d3f93c2"}, + {file = "mypy-1.13.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a4c1bfcdbce96ff5d96fc9b08e3831acb30dc44ab02671eca5953eadad07d6d0"}, + {file = "mypy-1.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:a0affb3a79a256b4183ba09811e3577c5163ed06685e4d4b46429a271ba174d2"}, + {file = "mypy-1.13.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a7b44178c9760ce1a43f544e595d35ed61ac2c3de306599fa59b38a6048e1aa7"}, + {file = "mypy-1.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5d5092efb8516d08440e36626f0153b5006d4088c1d663d88bf79625af3d1d62"}, + {file = "mypy-1.13.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de2904956dac40ced10931ac967ae63c5089bd498542194b436eb097a9f77bc8"}, + {file = "mypy-1.13.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:7bfd8836970d33c2105562650656b6846149374dc8ed77d98424b40b09340ba7"}, + {file = "mypy-1.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:9f73dba9ec77acb86457a8fc04b5239822df0c14a082564737833d2963677dbc"}, + {file = "mypy-1.13.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:100fac22ce82925f676a734af0db922ecfea991e1d7ec0ceb1e115ebe501301a"}, + {file = "mypy-1.13.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7bcb0bb7f42a978bb323a7c88f1081d1b5dee77ca86f4100735a6f541299d8fb"}, + {file = "mypy-1.13.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bde31fc887c213e223bbfc34328070996061b0833b0a4cfec53745ed61f3519b"}, + {file = "mypy-1.13.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:07de989f89786f62b937851295ed62e51774722e5444a27cecca993fc3f9cd74"}, + {file = "mypy-1.13.0-cp38-cp38-win_amd64.whl", hash = "sha256:4bde84334fbe19bad704b3f5b78c4abd35ff1026f8ba72b29de70dda0916beb6"}, + {file = "mypy-1.13.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:0246bcb1b5de7f08f2826451abd947bf656945209b140d16ed317f65a17dc7dc"}, + {file = "mypy-1.13.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:7f5b7deae912cf8b77e990b9280f170381fdfbddf61b4ef80927edd813163732"}, + {file = "mypy-1.13.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7029881ec6ffb8bc233a4fa364736789582c738217b133f1b55967115288a2bc"}, + {file = "mypy-1.13.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:3e38b980e5681f28f033f3be86b099a247b13c491f14bb8b1e1e134d23bb599d"}, + {file = "mypy-1.13.0-cp39-cp39-win_amd64.whl", hash = "sha256:a6789be98a2017c912ae6ccb77ea553bbaf13d27605d2ca20a76dfbced631b24"}, + {file = "mypy-1.13.0-py3-none-any.whl", hash = "sha256:9c250883f9fd81d212e0952c92dbfcc96fc237f4b7c92f56ac81fd48460b3e5a"}, + {file = "mypy-1.13.0.tar.gz", hash = "sha256:0291a61b6fbf3e6673e3405cfcc0e7650bebc7939659fdca2702958038bd835e"}, +] + +[package.dependencies] +mypy-extensions = ">=1.0.0" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} +typing-extensions = ">=4.6.0" + +[package.extras] +dmypy = ["psutil (>=4.0)"] +faster-cache = ["orjson"] +install-types = ["pip"] +mypyc = ["setuptools (>=50)"] +reports = ["lxml"] + +[[package]] +name = "mypy-extensions" +version = "1.1.0" +description = "Type system extensions for programs checked with the mypy type checker." +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505"}, + {file = "mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558"}, +] + +[[package]] +name = "packaging" +version = "25.0" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484"}, + {file = "packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"}, +] + +[[package]] +name = "pluggy" +version = "1.5.0" +description = "plugin and hook calling mechanisms for python" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, + {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + +[[package]] +name = "pydantic" +version = "2.10.6" +description = "Data validation using Python type hints" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "pydantic-2.10.6-py3-none-any.whl", hash = "sha256:427d664bf0b8a2b34ff5dd0f5a18df00591adcee7198fbd71981054cef37b584"}, + {file = "pydantic-2.10.6.tar.gz", hash = "sha256:ca5daa827cce33de7a42be142548b0096bf05a7e7b365aebfa5f8eeec7128236"}, +] + +[package.dependencies] +annotated-types = ">=0.6.0" +pydantic-core = "2.27.2" +typing-extensions = ">=4.12.2" + +[package.extras] +email = ["email-validator (>=2.0.0)"] +timezone = ["tzdata ; python_version >= \"3.9\" and platform_system == \"Windows\""] + +[[package]] +name = "pydantic-core" +version = "2.27.2" +description = "Core functionality for Pydantic validation and serialization" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "pydantic_core-2.27.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2d367ca20b2f14095a8f4fa1210f5a7b78b8a20009ecced6b12818f455b1e9fa"}, + {file = "pydantic_core-2.27.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:491a2b73db93fab69731eaee494f320faa4e093dbed776be1a829c2eb222c34c"}, + {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7969e133a6f183be60e9f6f56bfae753585680f3b7307a8e555a948d443cc05a"}, + {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3de9961f2a346257caf0aa508a4da705467f53778e9ef6fe744c038119737ef5"}, + {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e2bb4d3e5873c37bb3dd58714d4cd0b0e6238cebc4177ac8fe878f8b3aa8e74c"}, + {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:280d219beebb0752699480fe8f1dc61ab6615c2046d76b7ab7ee38858de0a4e7"}, + {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47956ae78b6422cbd46f772f1746799cbb862de838fd8d1fbd34a82e05b0983a"}, + {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:14d4a5c49d2f009d62a2a7140d3064f686d17a5d1a268bc641954ba181880236"}, + {file = "pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:337b443af21d488716f8d0b6164de833e788aa6bd7e3a39c005febc1284f4962"}, + {file = "pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:03d0f86ea3184a12f41a2d23f7ccb79cdb5a18e06993f8a45baa8dfec746f0e9"}, + {file = "pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7041c36f5680c6e0f08d922aed302e98b3745d97fe1589db0a3eebf6624523af"}, + {file = "pydantic_core-2.27.2-cp310-cp310-win32.whl", hash = "sha256:50a68f3e3819077be2c98110c1f9dcb3817e93f267ba80a2c05bb4f8799e2ff4"}, + {file = "pydantic_core-2.27.2-cp310-cp310-win_amd64.whl", hash = "sha256:e0fd26b16394ead34a424eecf8a31a1f5137094cabe84a1bcb10fa6ba39d3d31"}, + {file = "pydantic_core-2.27.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:8e10c99ef58cfdf2a66fc15d66b16c4a04f62bca39db589ae8cba08bc55331bc"}, + {file = "pydantic_core-2.27.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:26f32e0adf166a84d0cb63be85c562ca8a6fa8de28e5f0d92250c6b7e9e2aff7"}, + {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c19d1ea0673cd13cc2f872f6c9ab42acc4e4f492a7ca9d3795ce2b112dd7e15"}, + {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5e68c4446fe0810e959cdff46ab0a41ce2f2c86d227d96dc3847af0ba7def306"}, + {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d9640b0059ff4f14d1f37321b94061c6db164fbe49b334b31643e0528d100d99"}, + {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:40d02e7d45c9f8af700f3452f329ead92da4c5f4317ca9b896de7ce7199ea459"}, + {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1c1fd185014191700554795c99b347d64f2bb637966c4cfc16998a0ca700d048"}, + {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d81d2068e1c1228a565af076598f9e7451712700b673de8f502f0334f281387d"}, + {file = "pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1a4207639fb02ec2dbb76227d7c751a20b1a6b4bc52850568e52260cae64ca3b"}, + {file = "pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:3de3ce3c9ddc8bbd88f6e0e304dea0e66d843ec9de1b0042b0911c1663ffd474"}, + {file = "pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:30c5f68ded0c36466acede341551106821043e9afaad516adfb6e8fa80a4e6a6"}, + {file = "pydantic_core-2.27.2-cp311-cp311-win32.whl", hash = "sha256:c70c26d2c99f78b125a3459f8afe1aed4d9687c24fd677c6a4436bc042e50d6c"}, + {file = "pydantic_core-2.27.2-cp311-cp311-win_amd64.whl", hash = "sha256:08e125dbdc505fa69ca7d9c499639ab6407cfa909214d500897d02afb816e7cc"}, + {file = "pydantic_core-2.27.2-cp311-cp311-win_arm64.whl", hash = "sha256:26f0d68d4b235a2bae0c3fc585c585b4ecc51382db0e3ba402a22cbc440915e4"}, + {file = "pydantic_core-2.27.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9e0c8cfefa0ef83b4da9588448b6d8d2a2bf1a53c3f1ae5fca39eb3061e2f0b0"}, + {file = "pydantic_core-2.27.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:83097677b8e3bd7eaa6775720ec8e0405f1575015a463285a92bfdfe254529ef"}, + {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:172fce187655fece0c90d90a678424b013f8fbb0ca8b036ac266749c09438cb7"}, + {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:519f29f5213271eeeeb3093f662ba2fd512b91c5f188f3bb7b27bc5973816934"}, + {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:05e3a55d124407fffba0dd6b0c0cd056d10e983ceb4e5dbd10dda135c31071d6"}, + {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9c3ed807c7b91de05e63930188f19e921d1fe90de6b4f5cd43ee7fcc3525cb8c"}, + {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fb4aadc0b9a0c063206846d603b92030eb6f03069151a625667f982887153e2"}, + {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:28ccb213807e037460326424ceb8b5245acb88f32f3d2777427476e1b32c48c4"}, + {file = "pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:de3cd1899e2c279b140adde9357c4495ed9d47131b4a4eaff9052f23398076b3"}, + {file = "pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:220f892729375e2d736b97d0e51466252ad84c51857d4d15f5e9692f9ef12be4"}, + {file = "pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a0fcd29cd6b4e74fe8ddd2c90330fd8edf2e30cb52acda47f06dd615ae72da57"}, + {file = "pydantic_core-2.27.2-cp312-cp312-win32.whl", hash = "sha256:1e2cb691ed9834cd6a8be61228471d0a503731abfb42f82458ff27be7b2186fc"}, + {file = "pydantic_core-2.27.2-cp312-cp312-win_amd64.whl", hash = "sha256:cc3f1a99a4f4f9dd1de4fe0312c114e740b5ddead65bb4102884b384c15d8bc9"}, + {file = "pydantic_core-2.27.2-cp312-cp312-win_arm64.whl", hash = "sha256:3911ac9284cd8a1792d3cb26a2da18f3ca26c6908cc434a18f730dc0db7bfa3b"}, + {file = "pydantic_core-2.27.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7d14bd329640e63852364c306f4d23eb744e0f8193148d4044dd3dacdaacbd8b"}, + {file = "pydantic_core-2.27.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82f91663004eb8ed30ff478d77c4d1179b3563df6cdb15c0817cd1cdaf34d154"}, + {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71b24c7d61131bb83df10cc7e687433609963a944ccf45190cfc21e0887b08c9"}, + {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fa8e459d4954f608fa26116118bb67f56b93b209c39b008277ace29937453dc9"}, + {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce8918cbebc8da707ba805b7fd0b382816858728ae7fe19a942080c24e5b7cd1"}, + {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eda3f5c2a021bbc5d976107bb302e0131351c2ba54343f8a496dc8783d3d3a6a"}, + {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd8086fa684c4775c27f03f062cbb9eaa6e17f064307e86b21b9e0abc9c0f02e"}, + {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8d9b3388db186ba0c099a6d20f0604a44eabdeef1777ddd94786cdae158729e4"}, + {file = "pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7a66efda2387de898c8f38c0cf7f14fca0b51a8ef0b24bfea5849f1b3c95af27"}, + {file = "pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:18a101c168e4e092ab40dbc2503bdc0f62010e95d292b27827871dc85450d7ee"}, + {file = "pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ba5dd002f88b78a4215ed2f8ddbdf85e8513382820ba15ad5ad8955ce0ca19a1"}, + {file = "pydantic_core-2.27.2-cp313-cp313-win32.whl", hash = "sha256:1ebaf1d0481914d004a573394f4be3a7616334be70261007e47c2a6fe7e50130"}, + {file = "pydantic_core-2.27.2-cp313-cp313-win_amd64.whl", hash = "sha256:953101387ecf2f5652883208769a79e48db18c6df442568a0b5ccd8c2723abee"}, + {file = "pydantic_core-2.27.2-cp313-cp313-win_arm64.whl", hash = "sha256:ac4dbfd1691affb8f48c2c13241a2e3b60ff23247cbcf981759c768b6633cf8b"}, + {file = "pydantic_core-2.27.2-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:d3e8d504bdd3f10835468f29008d72fc8359d95c9c415ce6e767203db6127506"}, + {file = "pydantic_core-2.27.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:521eb9b7f036c9b6187f0b47318ab0d7ca14bd87f776240b90b21c1f4f149320"}, + {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:85210c4d99a0114f5a9481b44560d7d1e35e32cc5634c656bc48e590b669b145"}, + {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d716e2e30c6f140d7560ef1538953a5cd1a87264c737643d481f2779fc247fe1"}, + {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f66d89ba397d92f840f8654756196d93804278457b5fbede59598a1f9f90b228"}, + {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:669e193c1c576a58f132e3158f9dfa9662969edb1a250c54d8fa52590045f046"}, + {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdbe7629b996647b99c01b37f11170a57ae675375b14b8c13b8518b8320ced5"}, + {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d262606bf386a5ba0b0af3b97f37c83d7011439e3dc1a9298f21efb292e42f1a"}, + {file = "pydantic_core-2.27.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:cabb9bcb7e0d97f74df8646f34fc76fbf793b7f6dc2438517d7a9e50eee4f14d"}, + {file = "pydantic_core-2.27.2-cp38-cp38-musllinux_1_1_armv7l.whl", hash = "sha256:d2d63f1215638d28221f664596b1ccb3944f6e25dd18cd3b86b0a4c408d5ebb9"}, + {file = "pydantic_core-2.27.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:bca101c00bff0adb45a833f8451b9105d9df18accb8743b08107d7ada14bd7da"}, + {file = "pydantic_core-2.27.2-cp38-cp38-win32.whl", hash = "sha256:f6f8e111843bbb0dee4cb6594cdc73e79b3329b526037ec242a3e49012495b3b"}, + {file = "pydantic_core-2.27.2-cp38-cp38-win_amd64.whl", hash = "sha256:fd1aea04935a508f62e0d0ef1f5ae968774a32afc306fb8545e06f5ff5cdf3ad"}, + {file = "pydantic_core-2.27.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:c10eb4f1659290b523af58fa7cffb452a61ad6ae5613404519aee4bfbf1df993"}, + {file = "pydantic_core-2.27.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ef592d4bad47296fb11f96cd7dc898b92e795032b4894dfb4076cfccd43a9308"}, + {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c61709a844acc6bf0b7dce7daae75195a10aac96a596ea1b776996414791ede4"}, + {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:42c5f762659e47fdb7b16956c71598292f60a03aa92f8b6351504359dbdba6cf"}, + {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4c9775e339e42e79ec99c441d9730fccf07414af63eac2f0e48e08fd38a64d76"}, + {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:57762139821c31847cfb2df63c12f725788bd9f04bc2fb392790959b8f70f118"}, + {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0d1e85068e818c73e048fe28cfc769040bb1f475524f4745a5dc621f75ac7630"}, + {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:097830ed52fd9e427942ff3b9bc17fab52913b2f50f2880dc4a5611446606a54"}, + {file = "pydantic_core-2.27.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:044a50963a614ecfae59bb1eaf7ea7efc4bc62f49ed594e18fa1e5d953c40e9f"}, + {file = "pydantic_core-2.27.2-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:4e0b4220ba5b40d727c7f879eac379b822eee5d8fff418e9d3381ee45b3b0362"}, + {file = "pydantic_core-2.27.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5e4f4bb20d75e9325cc9696c6802657b58bc1dbbe3022f32cc2b2b632c3fbb96"}, + {file = "pydantic_core-2.27.2-cp39-cp39-win32.whl", hash = "sha256:cca63613e90d001b9f2f9a9ceb276c308bfa2a43fafb75c8031c4f66039e8c6e"}, + {file = "pydantic_core-2.27.2-cp39-cp39-win_amd64.whl", hash = "sha256:77d1bca19b0f7021b3a982e6f903dcd5b2b06076def36a652e3907f596e29f67"}, + {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:2bf14caea37e91198329b828eae1618c068dfb8ef17bb33287a7ad4b61ac314e"}, + {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:b0cb791f5b45307caae8810c2023a184c74605ec3bcbb67d13846c28ff731ff8"}, + {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:688d3fd9fcb71f41c4c015c023d12a79d1c4c0732ec9eb35d96e3388a120dcf3"}, + {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d591580c34f4d731592f0e9fe40f9cc1b430d297eecc70b962e93c5c668f15f"}, + {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:82f986faf4e644ffc189a7f1aafc86e46ef70372bb153e7001e8afccc6e54133"}, + {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:bec317a27290e2537f922639cafd54990551725fc844249e64c523301d0822fc"}, + {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:0296abcb83a797db256b773f45773da397da75a08f5fcaef41f2044adec05f50"}, + {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:0d75070718e369e452075a6017fbf187f788e17ed67a3abd47fa934d001863d9"}, + {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:7e17b560be3c98a8e3aa66ce828bdebb9e9ac6ad5466fba92eb74c4c95cb1151"}, + {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c33939a82924da9ed65dab5a65d427205a73181d8098e79b6b426bdf8ad4e656"}, + {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:00bad2484fa6bda1e216e7345a798bd37c68fb2d97558edd584942aa41b7d278"}, + {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c817e2b40aba42bac6f457498dacabc568c3b7a986fc9ba7c8d9d260b71485fb"}, + {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:251136cdad0cb722e93732cb45ca5299fb56e1344a833640bf93b2803f8d1bfd"}, + {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d2088237af596f0a524d3afc39ab3b036e8adb054ee57cbb1dcf8e09da5b29cc"}, + {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:d4041c0b966a84b4ae7a09832eb691a35aec90910cd2dbe7a208de59be77965b"}, + {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:8083d4e875ebe0b864ffef72a4304827015cff328a1be6e22cc850753bfb122b"}, + {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f141ee28a0ad2123b6611b6ceff018039df17f32ada8b534e6aa039545a3efb2"}, + {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7d0c8399fcc1848491f00e0314bd59fb34a9c008761bcb422a057670c3f65e35"}, + {file = "pydantic_core-2.27.2.tar.gz", hash = "sha256:eb026e5a4c1fee05726072337ff51d1efb6f59090b7da90d30ea58625b1ffb39"}, +] + +[package.dependencies] +typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" + +[[package]] +name = "pytest" +version = "7.4.4" +description = "pytest: simple powerful testing with Python" +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "pytest-7.4.4-py3-none-any.whl", hash = "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8"}, + {file = "pytest-7.4.4.tar.gz", hash = "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "sys_platform == \"win32\""} +exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=0.12,<2.0" +tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} + +[package.extras] +testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] + +[[package]] +name = "pytest-asyncio" +version = "0.23.8" +description = "Pytest support for asyncio" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "pytest_asyncio-0.23.8-py3-none-any.whl", hash = "sha256:50265d892689a5faefb84df80819d1ecef566eb3549cf915dfb33569359d1ce2"}, + {file = "pytest_asyncio-0.23.8.tar.gz", hash = "sha256:759b10b33a6dc61cce40a8bd5205e302978bbbcc00e279a8b61d9a6a3c82e4d3"}, +] + +[package.dependencies] +pytest = ">=7.0.0,<9" + +[package.extras] +docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1.0)"] +testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)"] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +description = "Extensions to the standard Python datetime module" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +groups = ["dev"] +files = [ + {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, + {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, +] + +[package.dependencies] +six = ">=1.5" + +[[package]] +name = "ruff" +version = "0.11.5" +description = "An extremely fast Python linter and code formatter, written in Rust." +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "ruff-0.11.5-py3-none-linux_armv6l.whl", hash = "sha256:2561294e108eb648e50f210671cc56aee590fb6167b594144401532138c66c7b"}, + {file = "ruff-0.11.5-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ac12884b9e005c12d0bd121f56ccf8033e1614f736f766c118ad60780882a077"}, + {file = "ruff-0.11.5-py3-none-macosx_11_0_arm64.whl", hash = "sha256:4bfd80a6ec559a5eeb96c33f832418bf0fb96752de0539905cf7b0cc1d31d779"}, + {file = "ruff-0.11.5-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0947c0a1afa75dcb5db4b34b070ec2bccee869d40e6cc8ab25aca11a7d527794"}, + {file = "ruff-0.11.5-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ad871ff74b5ec9caa66cb725b85d4ef89b53f8170f47c3406e32ef040400b038"}, + {file = "ruff-0.11.5-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e6cf918390cfe46d240732d4d72fa6e18e528ca1f60e318a10835cf2fa3dc19f"}, + {file = "ruff-0.11.5-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:56145ee1478582f61c08f21076dc59153310d606ad663acc00ea3ab5b2125f82"}, + {file = "ruff-0.11.5-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e5f66f8f1e8c9fc594cbd66fbc5f246a8d91f916cb9667e80208663ec3728304"}, + {file = "ruff-0.11.5-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:80b4df4d335a80315ab9afc81ed1cff62be112bd165e162b5eed8ac55bfc8470"}, + {file = "ruff-0.11.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3068befab73620b8a0cc2431bd46b3cd619bc17d6f7695a3e1bb166b652c382a"}, + {file = "ruff-0.11.5-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:f5da2e710a9641828e09aa98b92c9ebbc60518fdf3921241326ca3e8f8e55b8b"}, + {file = "ruff-0.11.5-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:ef39f19cb8ec98cbc762344921e216f3857a06c47412030374fffd413fb8fd3a"}, + {file = "ruff-0.11.5-py3-none-musllinux_1_2_i686.whl", hash = "sha256:b2a7cedf47244f431fd11aa5a7e2806dda2e0c365873bda7834e8f7d785ae159"}, + {file = "ruff-0.11.5-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:81be52e7519f3d1a0beadcf8e974715b2dfc808ae8ec729ecfc79bddf8dbb783"}, + {file = "ruff-0.11.5-py3-none-win32.whl", hash = "sha256:e268da7b40f56e3eca571508a7e567e794f9bfcc0f412c4b607931d3af9c4afe"}, + {file = "ruff-0.11.5-py3-none-win_amd64.whl", hash = "sha256:6c6dc38af3cfe2863213ea25b6dc616d679205732dc0fb673356c2d69608f800"}, + {file = "ruff-0.11.5-py3-none-win_arm64.whl", hash = "sha256:67e241b4314f4eacf14a601d586026a962f4002a475aa702c69980a38087aa4e"}, + {file = "ruff-0.11.5.tar.gz", hash = "sha256:cae2e2439cb88853e421901ec040a758960b576126dab520fa08e9de431d1bef"}, +] + +[[package]] +name = "six" +version = "1.17.0" +description = "Python 2 and 3 compatibility utilities" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +groups = ["dev"] +files = [ + {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"}, + {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +description = "Sniff out which async library your code is running under" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, + {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, +] + +[[package]] +name = "tomli" +version = "2.2.1" +description = "A lil' TOML parser" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +markers = "python_version < \"3.11\"" +files = [ + {file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"}, + {file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"}, + {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a"}, + {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee"}, + {file = "tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e"}, + {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4"}, + {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106"}, + {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8"}, + {file = "tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff"}, + {file = "tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b"}, + {file = "tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea"}, + {file = "tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8"}, + {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192"}, + {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222"}, + {file = "tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77"}, + {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6"}, + {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd"}, + {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e"}, + {file = "tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98"}, + {file = "tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4"}, + {file = "tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7"}, + {file = "tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c"}, + {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13"}, + {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281"}, + {file = "tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272"}, + {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140"}, + {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2"}, + {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744"}, + {file = "tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec"}, + {file = "tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69"}, + {file = "tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc"}, + {file = "tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff"}, +] + +[[package]] +name = "types-python-dateutil" +version = "2.9.0.20241206" +description = "Typing stubs for python-dateutil" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "types_python_dateutil-2.9.0.20241206-py3-none-any.whl", hash = "sha256:e248a4bc70a486d3e3ec84d0dc30eec3a5f979d6e7ee4123ae043eedbb987f53"}, + {file = "types_python_dateutil-2.9.0.20241206.tar.gz", hash = "sha256:18f493414c26ffba692a72369fea7a154c502646301ebfe3d56a04b3767284cb"}, +] + +[[package]] +name = "typing-extensions" +version = "4.13.2" +description = "Backported and Experimental Type Hints for Python 3.8+" +optional = false +python-versions = ">=3.8" +groups = ["main", "dev"] +files = [ + {file = "typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c"}, + {file = "typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef"}, +] + +[metadata] +lock-version = "2.1" +python-versions = "^3.8" +content-hash = "8551b871abee465e23fb0966d51f2c155fd257b55bdcb0c02d095de19f92f358" diff --git a/pyproject.toml b/pyproject.toml index cbc8b6a..b3ac656 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,211 +1,84 @@ [project] -name = "browser-use-sdk" -version = "1.0.2" -description = "The official Python library for the browser-use API" -dynamic = ["readme"] -license = "Apache-2.0" -authors = [ -{ name = "Browser Use", email = "support@browser-use.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", -] -requires-python = ">= 3.8" -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", - "Programming Language :: Python :: 3.12", - "Programming Language :: Python :: 3.13", - "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" -] +name = "browser-use" -[project.urls] -Homepage = "https://github.com/browser-use/browser-use-python" -Repository = "https://github.com/browser-use/browser-use-python" - -[project.optional-dependencies] -aiohttp = ["aiohttp", "httpx_aiohttp>=0.1.8"] +[tool.poetry] +name = "browser-use" +version = "0.0.0" +description = "" +readme = "README.md" +authors = [] +keywords = [] -[tool.rye] -managed = true -# version pins are in requirements-dev.lock -dev-dependencies = [ - "pyright==1.1.399", - "mypy", - "respx", - "pytest", - "pytest-asyncio", - "ruff", - "time-machine", - "nox", - "dirty-equals>=0.6.0", - "importlib-metadata>=6.7.0", - "rich>=13.7.1", - "nest_asyncio==1.6.0", - "pytest-xdist>=3.6.1", +classifiers = [ + "Intended Audience :: Developers", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "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", + "Typing :: Typed" ] - -[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 browser_use_sdk'" - -typecheck = { chain = [ - "typecheck:pyright", - "typecheck:mypy" -]} -"typecheck:pyright" = "pyright" -"typecheck:verify-types" = "pyright --verifytypes browser_use_sdk --ignoreexternal" -"typecheck:mypy" = "mypy ." - -[build-system] -requires = ["hatchling==1.26.3", "hatch-fancy-pypi-readme"] -build-backend = "hatchling.build" - -[tool.hatch.build] -include = [ - "src/*" +packages = [ + { include = "browser_use", from = "src"} ] -[tool.hatch.build.targets.wheel] -packages = ["src/browser_use_sdk"] - -[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/browser-use/browser-use-python/tree/main/\g<2>)' +[project.urls] +Repository = 'https://github.com/browser-use/browser-use-python' + +[tool.poetry.dependencies] +python = "^3.8" +httpx = ">=0.21.2" +pydantic = ">= 1.9.2" +pydantic-core = ">=2.18.2" +typing_extensions = ">= 4.0.0" + +[tool.poetry.group.dev.dependencies] +mypy = "==1.13.0" +pytest = "^7.4.0" +pytest-asyncio = "^0.23.5" +python-dateutil = "^2.9.0" +types-python-dateutil = "^2.9.0.20240316" +ruff = "==0.11.5" [tool.pytest.ini_options] -testpaths = ["tests"] -addopts = "--tb=short -n auto" -xfail_strict = true +testpaths = [ "tests" ] asyncio_mode = "auto" -asyncio_default_fixture_loop_scope = "session" -filterwarnings = [ - "error" -] - -[tool.pyright] -# this enables practically every flag given by pyright. -# there are a couple of flags that are still disabled by -# default in strict mode as they are experimental and niche. -typeCheckingMode = "strict" -pythonVersion = "3.8" -exclude = [ - "_dev", - ".venv", - ".nox", -] - -reportImplicitOverride = true -reportOverlappingOverload = false - -reportImportCycles = false -reportPrivateUsage = false +[tool.mypy] +plugins = ["pydantic.mypy"] [tool.ruff] line-length = 120 -output-format = "grouped" -target-version = "py38" - -[tool.ruff.format] -docstring-code-format = true [tool.ruff.lint] select = [ - # isort - "I", - # bugbear rules - "B", - # remove unused imports - "F401", - # bare except statements - "E722", - # unused arguments - "ARG", - # print statements - "T201", - "T203", - # misuse of typing.TYPE_CHECKING - "TC004", - # import rules - "TID251", + "E", # pycodestyle errors + "F", # pyflakes + "I", # isort ] ignore = [ - # mutable defaults - "B006", + "E402", # Module level import not at top of file + "E501", # Line too long + "E711", # Comparison to `None` should be `cond is not None` + "E712", # Avoid equality comparisons to `True`; use `if ...:` checks + "E721", # Use `is` and `is not` for type comparisons, or `isinstance()` for insinstance checks + "E722", # Do not use bare `except` + "E731", # Do not assign a `lambda` expression, use a `def` + "F821", # Undefined name + "F841" # Local variable ... is assigned to but never used ] -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 = ["browser_use_sdk", "tests"] +section-order = ["future", "standard-library", "third-party", "first-party"] -[tool.ruff.lint.per-file-ignores] -"bin/**.py" = ["T201", "T203"] -"scripts/**.py" = ["T201", "T203"] -"tests/**.py" = ["T201", "T203"] -"examples/**.py" = ["T201", "T203"] +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" diff --git a/reference.md b/reference.md new file mode 100644 index 0000000..33df870 --- /dev/null +++ b/reference.md @@ -0,0 +1,1606 @@ +# Reference +## Accounts +
client.accounts.get_account_me() +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Get authenticated account information including credit balances and account details. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from browser_use import BrowserUse + +client = BrowserUse( + api_key="YOUR_API_KEY", +) +client.accounts.get_account_me() + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ + +
+
+
+ +## Tasks +
client.tasks.list_tasks(...) +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Get paginated list of AI agent tasks with optional filtering by session and status. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from browser_use import BrowserUse + +client = BrowserUse( + api_key="YOUR_API_KEY", +) +client.tasks.list_tasks() + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**page_size:** `typing.Optional[int]` + +
+
+ +
+
+ +**page_number:** `typing.Optional[int]` + +
+
+ +
+
+ +**session_id:** `typing.Optional[str]` + +
+
+ +
+
+ +**filter_by:** `typing.Optional[TaskStatus]` + +
+
+ +
+
+ +**after:** `typing.Optional[dt.datetime]` + +
+
+ +
+
+ +**before:** `typing.Optional[dt.datetime]` + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ + +
+
+
+ +
client.tasks.create_task(...) +
+
+ +#### 📝 Description + +
+
+ +
+
+ +You can either: +1. Start a new task (auto creates a new simple session) +2. Start a new task in an existing session (you can create a custom session before starting the task and reuse it for follow-up tasks) +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from browser_use import BrowserUse + +client = BrowserUse( + api_key="YOUR_API_KEY", +) +client.tasks.create_task( + task="task", +) + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**task:** `str` — The task prompt/instruction for the agent. + +
+
+ +
+
+ +**llm:** `typing.Optional[SupportedLlMs]` — The LLM model to use for the agent. + +
+
+ +
+
+ +**start_url:** `typing.Optional[str]` — The URL to start the task from. + +
+
+ +
+
+ +**max_steps:** `typing.Optional[int]` — Maximum number of steps the agent can take before stopping. + +
+
+ +
+
+ +**structured_output:** `typing.Optional[str]` — The stringified JSON schema for the structured output. + +
+
+ +
+
+ +**session_id:** `typing.Optional[str]` — The ID of the session where the task will run. + +
+
+ +
+
+ +**metadata:** `typing.Optional[typing.Dict[str, typing.Optional[str]]]` — The metadata for the task. + +
+
+ +
+
+ +**secrets:** `typing.Optional[typing.Dict[str, typing.Optional[str]]]` — The secrets for the task. + +
+
+ +
+
+ +**allowed_domains:** `typing.Optional[typing.Sequence[str]]` — The allowed domains for the task. + +
+
+ +
+
+ +**highlight_elements:** `typing.Optional[bool]` — Tells the agent to highlight interactive elements on the page. + +
+
+ +
+
+ +**flash_mode:** `typing.Optional[bool]` — Whether agent flash mode is enabled. + +
+
+ +
+
+ +**thinking:** `typing.Optional[bool]` — Whether agent thinking mode is enabled. + +
+
+ +
+
+ +**vision:** `typing.Optional[bool]` — Whether agent vision capabilities are enabled. + +
+
+ +
+
+ +**system_prompt_extension:** `typing.Optional[str]` — Optional extension to the agent system prompt. + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ + +
+
+
+ +
client.tasks.get_task(...) +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Get detailed task information including status, progress, steps, and file outputs. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from browser_use import BrowserUse + +client = BrowserUse( + api_key="YOUR_API_KEY", +) +client.tasks.get_task( + task_id="task_id", +) + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**task_id:** `str` + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ + +
+
+
+ +
client.tasks.update_task(...) +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Control task execution with stop, pause, resume, or stop task and session actions. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from browser_use import BrowserUse + +client = BrowserUse( + api_key="YOUR_API_KEY", +) +client.tasks.update_task( + task_id="task_id", + action="stop", +) + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**task_id:** `str` + +
+
+ +
+
+ +**action:** `TaskUpdateAction` — The action to perform on the task + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ + +
+
+
+ +
client.tasks.get_task_logs(...) +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Get secure download URL for task execution logs with step-by-step details. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from browser_use import BrowserUse + +client = BrowserUse( + api_key="YOUR_API_KEY", +) +client.tasks.get_task_logs( + task_id="task_id", +) + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**task_id:** `str` + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ + +
+
+
+ +## Sessions +
client.sessions.list_sessions(...) +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Get paginated list of AI agent sessions with optional status filtering. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from browser_use import BrowserUse + +client = BrowserUse( + api_key="YOUR_API_KEY", +) +client.sessions.list_sessions() + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**page_size:** `typing.Optional[int]` + +
+
+ +
+
+ +**page_number:** `typing.Optional[int]` + +
+
+ +
+
+ +**filter_by:** `typing.Optional[SessionStatus]` + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ + +
+
+
+ +
client.sessions.create_session(...) +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Create a new session with a new task. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from browser_use import BrowserUse + +client = BrowserUse( + api_key="YOUR_API_KEY", +) +client.sessions.create_session() + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**profile_id:** `typing.Optional[str]` — The ID of the profile to use for the session + +
+
+ +
+
+ +**proxy_country_code:** `typing.Optional[ProxyCountryCode]` — Country code for proxy location. + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ + +
+
+
+ +
client.sessions.get_session(...) +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Get detailed session information including status, URLs, and task details. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from browser_use import BrowserUse + +client = BrowserUse( + api_key="YOUR_API_KEY", +) +client.sessions.get_session( + session_id="session_id", +) + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**session_id:** `str` + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ + +
+
+
+ +
client.sessions.delete_session(...) +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Permanently delete a session and all associated data. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from browser_use import BrowserUse + +client = BrowserUse( + api_key="YOUR_API_KEY", +) +client.sessions.delete_session( + session_id="session_id", +) + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**session_id:** `str` + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ + +
+
+
+ +
client.sessions.update_session(...) +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Stop a session and all its running tasks. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from browser_use import BrowserUse + +client = BrowserUse( + api_key="YOUR_API_KEY", +) +client.sessions.update_session( + session_id="session_id", +) + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**session_id:** `str` + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ + +
+
+
+ +
client.sessions.get_session_public_share(...) +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Get public share information including URL and usage statistics. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from browser_use import BrowserUse + +client = BrowserUse( + api_key="YOUR_API_KEY", +) +client.sessions.get_session_public_share( + session_id="session_id", +) + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**session_id:** `str` + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ + +
+
+
+ +
client.sessions.create_session_public_share(...) +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Create or return existing public share for a session. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from browser_use import BrowserUse + +client = BrowserUse( + api_key="YOUR_API_KEY", +) +client.sessions.create_session_public_share( + session_id="session_id", +) + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**session_id:** `str` + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ + +
+
+
+ +
client.sessions.delete_session_public_share(...) +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Remove public share for a session. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from browser_use import BrowserUse + +client = BrowserUse( + api_key="YOUR_API_KEY", +) +client.sessions.delete_session_public_share( + session_id="session_id", +) + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**session_id:** `str` + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ + +
+
+
+ +## Files +
client.files.user_upload_file_presigned_url(...) +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Generate a secure presigned URL for uploading files that AI agents can use during tasks. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from browser_use import BrowserUse + +client = BrowserUse( + api_key="YOUR_API_KEY", +) +client.files.user_upload_file_presigned_url( + session_id="session_id", + file_name="fileName", + content_type="image/jpg", + size_bytes=1, +) + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**session_id:** `str` + +
+
+ +
+
+ +**file_name:** `str` — The name of the file to upload + +
+
+ +
+
+ +**content_type:** `UploadFileRequestContentType` — The content type of the file to upload + +
+
+ +
+
+ +**size_bytes:** `int` + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ + +
+
+
+ +
client.files.get_task_output_file_presigned_url(...) +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Get secure download URL for an output file generated by the AI agent. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from browser_use import BrowserUse + +client = BrowserUse( + api_key="YOUR_API_KEY", +) +client.files.get_task_output_file_presigned_url( + task_id="task_id", + file_id="file_id", +) + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**task_id:** `str` + +
+
+ +
+
+ +**file_id:** `str` + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ + +
+
+
+ +## Profiles +
client.profiles.list_profiles(...) +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Get paginated list of profiles. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from browser_use import BrowserUse + +client = BrowserUse( + api_key="YOUR_API_KEY", +) +client.profiles.list_profiles() + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**page_size:** `typing.Optional[int]` + +
+
+ +
+
+ +**page_number:** `typing.Optional[int]` + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ + +
+
+
+ +
client.profiles.create_profile() +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Profiles allow you to preserve the state of the browser between tasks. + +They are most commonly used to allow users to preserve the log-in state in the agent between tasks. +You'd normally create one profile per user and then use it for all their tasks. + +You can create a new profile by calling this endpoint. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from browser_use import BrowserUse + +client = BrowserUse( + api_key="YOUR_API_KEY", +) +client.profiles.create_profile() + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ + +
+
+
+ +
client.profiles.get_profile(...) +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Get profile details. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from browser_use import BrowserUse + +client = BrowserUse( + api_key="YOUR_API_KEY", +) +client.profiles.get_profile( + profile_id="profile_id", +) + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**profile_id:** `str` + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ + +
+
+
+ +
client.profiles.delete_browser_profile(...) +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Permanently delete a browser profile and its configuration. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from browser_use import BrowserUse + +client = BrowserUse( + api_key="YOUR_API_KEY", +) +client.profiles.delete_browser_profile( + profile_id="profile_id", +) + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**profile_id:** `str` + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ + +
+
+
+ diff --git a/release-please-config.json b/release-please-config.json deleted file mode 100644 index 1225639..0000000 --- a/release-please-config.json +++ /dev/null @@ -1,66 +0,0 @@ -{ - "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/browser_use_sdk/_version.py" - ] -} \ No newline at end of file diff --git a/requirements-dev.lock b/requirements-dev.lock deleted file mode 100644 index 57a6b07..0000000 --- a/requirements-dev.lock +++ /dev/null @@ -1,135 +0,0 @@ -# generated by rye -# use `rye lock` or `rye sync` to update this lockfile -# -# last locked with the following flags: -# pre: false -# features: [] -# all-features: true -# with-sources: false -# generate-hashes: false -# universal: false - --e file:. -aiohappyeyeballs==2.6.1 - # via aiohttp -aiohttp==3.12.8 - # via browser-use-sdk - # via httpx-aiohttp -aiosignal==1.3.2 - # via aiohttp -annotated-types==0.6.0 - # via pydantic -anyio==4.4.0 - # via browser-use-sdk - # 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 -colorlog==6.7.0 - # via nox -dirty-equals==0.6.0 -distlib==0.3.7 - # via virtualenv -distro==1.8.0 - # via browser-use-sdk -exceptiongroup==1.2.2 - # via anyio - # via pytest -execnet==2.1.1 - # via pytest-xdist -filelock==3.12.4 - # via virtualenv -frozenlist==1.6.2 - # via aiohttp - # via aiosignal -h11==0.16.0 - # via httpcore -httpcore==1.0.9 - # via httpx -httpx==0.28.1 - # via browser-use-sdk - # via httpx-aiohttp - # via respx -httpx-aiohttp==0.1.8 - # via browser-use-sdk -idna==3.4 - # via anyio - # via httpx - # via yarl -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 -multidict==6.4.4 - # via aiohttp - # via yarl -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 -packaging==23.2 - # via nox - # via pytest -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 browser-use-sdk -pydantic-core==2.27.1 - # via pydantic -pygments==2.18.0 - # via rich -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 - # 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 - # via python-dateutil -sniffio==1.3.0 - # via anyio - # via browser-use-sdk -time-machine==2.9.0 -tomli==2.0.2 - # via mypy - # via pytest -typing-extensions==4.12.2 - # via anyio - # via browser-use-sdk - # 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 deleted file mode 100644 index bd72e76..0000000 --- a/requirements.lock +++ /dev/null @@ -1,72 +0,0 @@ -# generated by rye -# use `rye lock` or `rye sync` to update this lockfile -# -# last locked with the following flags: -# pre: false -# features: [] -# all-features: true -# with-sources: false -# generate-hashes: false -# universal: false - --e file:. -aiohappyeyeballs==2.6.1 - # via aiohttp -aiohttp==3.12.8 - # via browser-use-sdk - # via httpx-aiohttp -aiosignal==1.3.2 - # via aiohttp -annotated-types==0.6.0 - # via pydantic -anyio==4.4.0 - # via browser-use-sdk - # via httpx -async-timeout==5.0.1 - # via aiohttp -attrs==25.3.0 - # via aiohttp -certifi==2023.7.22 - # via httpcore - # via httpx -distro==1.8.0 - # via browser-use-sdk -exceptiongroup==1.2.2 - # via anyio -frozenlist==1.6.2 - # via aiohttp - # via aiosignal -h11==0.16.0 - # via httpcore -httpcore==1.0.9 - # via httpx -httpx==0.28.1 - # via browser-use-sdk - # via httpx-aiohttp -httpx-aiohttp==0.1.8 - # via browser-use-sdk -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 browser-use-sdk -pydantic-core==2.27.1 - # via pydantic -sniffio==1.3.0 - # via anyio - # via browser-use-sdk -typing-extensions==4.12.2 - # via anyio - # via browser-use-sdk - # via multidict - # via pydantic - # via pydantic-core -yarl==1.20.0 - # via aiohttp diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..e80f640 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +httpx>=0.21.2 +pydantic>= 1.9.2 +pydantic-core>=2.18.2 +typing_extensions>= 4.0.0 diff --git a/scripts/bootstrap b/scripts/bootstrap deleted file mode 100755 index e84fe62..0000000 --- a/scripts/bootstrap +++ /dev/null @@ -1,19 +0,0 @@ -#!/usr/bin/env bash - -set -e - -cd "$(dirname "$0")/.." - -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 - } -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 deleted file mode 100755 index 667ec2d..0000000 --- a/scripts/format +++ /dev/null @@ -1,8 +0,0 @@ -#!/usr/bin/env bash - -set -e - -cd "$(dirname "$0")/.." - -echo "==> Running formatters" -rye run format diff --git a/scripts/lint b/scripts/lint deleted file mode 100755 index 3094ad3..0000000 --- a/scripts/lint +++ /dev/null @@ -1,11 +0,0 @@ -#!/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 browser_use_sdk' diff --git a/scripts/mock b/scripts/mock deleted file mode 100755 index 0b28f6e..0000000 --- a/scripts/mock +++ /dev/null @@ -1,41 +0,0 @@ -#!/usr/bin/env bash - -set -e - -cd "$(dirname "$0")/.." - -if [[ -n "$1" && "$1" != '--'* ]]; then - URL="$1" - shift -else - URL="$(grep 'openapi_spec_url' .stats.yml | cut -d' ' -f2)" -fi - -# Check if the URL is empty -if [ -z "$URL" ]; then - echo "Error: No OpenAPI spec path/url provided or found in .stats.yml" - exit 1 -fi - -echo "==> Starting mock server with URL ${URL}" - -# Run prism mock on the given spec -if [ "$1" == "--daemon" ]; then - npm exec --package=@stainless-api/prism-cli@5.15.0 -- prism mock "$URL" &> .prism.log & - - # Wait for server to come online - echo -n "Waiting for server" - while ! grep -q "✖ fatal\|Prism is listening" ".prism.log" ; do - echo -n "." - sleep 0.1 - done - - if grep -q "✖ fatal" ".prism.log"; then - cat .prism.log - exit 1 - fi - - echo -else - npm exec --package=@stainless-api/prism-cli@5.15.0 -- prism mock "$URL" -fi diff --git a/scripts/test b/scripts/test deleted file mode 100755 index dbeda2d..0000000 --- a/scripts/test +++ /dev/null @@ -1,61 +0,0 @@ -#!/usr/bin/env bash - -set -e - -cd "$(dirname "$0")/.." - -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[0;33m' -NC='\033[0m' # No Color - -function prism_is_running() { - curl --silent "http://localhost:4010" >/dev/null 2>&1 -} - -kill_server_on_port() { - pids=$(lsof -t -i tcp:"$1" || echo "") - if [ "$pids" != "" ]; then - kill "$pids" - echo "Stopped $pids." - fi -} - -function is_overriding_api_base_url() { - [ -n "$TEST_API_BASE_URL" ] -} - -if ! is_overriding_api_base_url && ! prism_is_running ; then - # When we exit this script, make sure to kill the background mock server process - trap 'kill_server_on_port 4010' EXIT - - # Start the dev server - ./scripts/mock --daemon -fi - -if is_overriding_api_base_url ; then - echo -e "${GREEN}✔ Running tests against ${TEST_API_BASE_URL}${NC}" - echo -elif ! prism_is_running ; then - echo -e "${RED}ERROR:${NC} The test suite will not run without a mock Prism server" - echo -e "running against your OpenAPI spec." - echo - echo -e "To run the server, pass in the path or url of your OpenAPI" - echo -e "spec to the prism command:" - echo - echo -e " \$ ${YELLOW}npm exec --package=@stainless-api/prism-cli@5.15.0 -- prism mock path/to/your.openapi.yml${NC}" - echo - - exit 1 -else - echo -e "${GREEN}✔ Mock prism server is running with your OpenAPI spec${NC}" - echo -fi - -export DEFER_PYDANTIC_BUILD=false - -echo "==> Running tests" -rye run pytest "$@" - -echo "==> Running Pydantic v1 tests" -rye run nox -s test-pydantic-v1 -- "$@" diff --git a/scripts/utils/ruffen-docs.py b/scripts/utils/ruffen-docs.py deleted file mode 100644 index 0cf2bd2..0000000 --- a/scripts/utils/ruffen-docs.py +++ /dev/null @@ -1,167 +0,0 @@ -# fork of https://github.com/asottile/blacken-docs adapted for ruff -from __future__ import annotations - -import re -import sys -import argparse -import textwrap -import contextlib -import subprocess -from typing import Match, Optional, Sequence, Generator, NamedTuple, cast - -MD_RE = re.compile( - r"(?P^(?P *)```\s*python\n)" r"(?P.*?)" r"(?P^(?P=indent)```\s*$)", - re.DOTALL | re.MULTILINE, -) -MD_PYCON_RE = re.compile( - r"(?P^(?P *)```\s*pycon\n)" r"(?P.*?)" r"(?P^(?P=indent)```.*$)", - re.DOTALL | re.MULTILINE, -) -PYCON_PREFIX = ">>> " -PYCON_CONTINUATION_PREFIX = "..." -PYCON_CONTINUATION_RE = re.compile( - rf"^{re.escape(PYCON_CONTINUATION_PREFIX)}( |$)", -) -DEFAULT_LINE_LENGTH = 100 - - -class CodeBlockError(NamedTuple): - offset: int - exc: Exception - - -def format_str( - src: str, -) -> tuple[str, Sequence[CodeBlockError]]: - errors: list[CodeBlockError] = [] - - @contextlib.contextmanager - def _collect_error(match: Match[str]) -> Generator[None, None, None]: - try: - yield - except Exception as e: - errors.append(CodeBlockError(match.start(), e)) - - def _md_match(match: Match[str]) -> str: - code = textwrap.dedent(match["code"]) - with _collect_error(match): - code = format_code_block(code) - code = textwrap.indent(code, match["indent"]) - return f"{match['before']}{code}{match['after']}" - - def _pycon_match(match: Match[str]) -> str: - code = "" - fragment = cast(Optional[str], None) - - def finish_fragment() -> None: - nonlocal code - nonlocal fragment - - if fragment is not None: - with _collect_error(match): - fragment = format_code_block(fragment) - fragment_lines = fragment.splitlines() - code += f"{PYCON_PREFIX}{fragment_lines[0]}\n" - for line in fragment_lines[1:]: - # Skip blank lines to handle Black adding a blank above - # functions within blocks. A blank line would end the REPL - # continuation prompt. - # - # >>> if True: - # ... def f(): - # ... pass - # ... - if line: - code += f"{PYCON_CONTINUATION_PREFIX} {line}\n" - if fragment_lines[-1].startswith(" "): - code += f"{PYCON_CONTINUATION_PREFIX}\n" - fragment = None - - indentation = None - for line in match["code"].splitlines(): - orig_line, line = line, line.lstrip() - if indentation is None and line: - indentation = len(orig_line) - len(line) - continuation_match = PYCON_CONTINUATION_RE.match(line) - if continuation_match and fragment is not None: - fragment += line[continuation_match.end() :] + "\n" - else: - finish_fragment() - if line.startswith(PYCON_PREFIX): - fragment = line[len(PYCON_PREFIX) :] + "\n" - else: - code += orig_line[indentation:] + "\n" - finish_fragment() - return code - - def _md_pycon_match(match: Match[str]) -> str: - code = _pycon_match(match) - code = textwrap.indent(code, match["indent"]) - return f"{match['before']}{code}{match['after']}" - - src = MD_RE.sub(_md_match, src) - src = MD_PYCON_RE.sub(_md_pycon_match, src) - return src, errors - - -def format_code_block(code: str) -> str: - return subprocess.check_output( - [ - sys.executable, - "-m", - "ruff", - "format", - "--stdin-filename=script.py", - f"--line-length={DEFAULT_LINE_LENGTH}", - ], - encoding="utf-8", - input=code, - ) - - -def format_file( - filename: str, - skip_errors: bool, -) -> int: - with open(filename, encoding="UTF-8") as f: - contents = f.read() - new_contents, errors = format_str(contents) - for error in errors: - lineno = contents[: error.offset].count("\n") + 1 - print(f"{filename}:{lineno}: code block parse error {error.exc}") - if errors and not skip_errors: - return 1 - if contents != new_contents: - print(f"{filename}: Rewriting...") - with open(filename, "w", encoding="UTF-8") as f: - f.write(new_contents) - return 0 - else: - return 0 - - -def main(argv: Sequence[str] | None = None) -> int: - parser = argparse.ArgumentParser() - parser.add_argument( - "-l", - "--line-length", - type=int, - default=DEFAULT_LINE_LENGTH, - ) - parser.add_argument( - "-S", - "--skip-string-normalization", - action="store_true", - ) - parser.add_argument("-E", "--skip-errors", action="store_true") - parser.add_argument("filenames", nargs="*") - args = parser.parse_args(argv) - - retv = 0 - for filename in args.filenames: - retv |= format_file(filename, skip_errors=args.skip_errors) - return retv - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/scripts/utils/upload-artifact.sh b/scripts/utils/upload-artifact.sh deleted file mode 100755 index 145854e..0000000 --- a/scripts/utils/upload-artifact.sh +++ /dev/null @@ -1,27 +0,0 @@ -#!/usr/bin/env bash -set -exuo pipefail - -FILENAME=$(basename dist/*.whl) - -RESPONSE=$(curl -X POST "$URL?filename=$FILENAME" \ - -H "Authorization: Bearer $AUTH" \ - -H "Content-Type: application/json") - -SIGNED_URL=$(echo "$RESPONSE" | jq -r '.url') - -if [[ "$SIGNED_URL" == "null" ]]; then - echo -e "\033[31mFailed to get signed URL.\033[0m" - exit 1 -fi - -UPLOAD_RESPONSE=$(curl -v -X PUT \ - -H "Content-Type: binary/octet-stream" \ - --data-binary "@dist/$FILENAME" "$SIGNED_URL" 2>&1) - -if echo "$UPLOAD_RESPONSE" | grep -q "HTTP/[0-9.]* 200"; then - echo -e "\033[32mUploaded build to Stainless storage.\033[0m" - echo -e "\033[32mInstallation: pip install 'https://pkg.stainless.com/s/browser-use-python/$SHA/$FILENAME'\033[0m" -else - echo -e "\033[31mFailed to upload artifact.\033[0m" - exit 1 -fi diff --git a/src/browser_use/__init__.py b/src/browser_use/__init__.py new file mode 100644 index 0000000..b415d1a --- /dev/null +++ b/src/browser_use/__init__.py @@ -0,0 +1,9 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +from . import accounts, files, profiles, sessions, tasks +from .client import AsyncBrowserUse, BrowserUse +from .version import __version__ + +__all__ = ["AsyncBrowserUse", "BrowserUse", "__version__", "accounts", "files", "profiles", "sessions", "tasks"] diff --git a/src/browser_use/accounts/__init__.py b/src/browser_use/accounts/__init__.py new file mode 100644 index 0000000..5cde020 --- /dev/null +++ b/src/browser_use/accounts/__init__.py @@ -0,0 +1,4 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + diff --git a/src/browser_use/accounts/client.py b/src/browser_use/accounts/client.py new file mode 100644 index 0000000..ba05a0e --- /dev/null +++ b/src/browser_use/accounts/client.py @@ -0,0 +1,100 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from ..core.request_options import RequestOptions +from ..types.account_view import AccountView +from .raw_client import AsyncRawAccountsClient, RawAccountsClient + + +class AccountsClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._raw_client = RawAccountsClient(client_wrapper=client_wrapper) + + @property + def with_raw_response(self) -> RawAccountsClient: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + RawAccountsClient + """ + return self._raw_client + + def get_account_me(self, *, request_options: typing.Optional[RequestOptions] = None) -> AccountView: + """ + Get authenticated account information including credit balances and account details. + + Parameters + ---------- + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AccountView + Successful Response + + Examples + -------- + from browser_use import BrowserUse + + client = BrowserUse( + api_key="YOUR_API_KEY", + ) + client.accounts.get_account_me() + """ + _response = self._raw_client.get_account_me(request_options=request_options) + return _response.data + + +class AsyncAccountsClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._raw_client = AsyncRawAccountsClient(client_wrapper=client_wrapper) + + @property + def with_raw_response(self) -> AsyncRawAccountsClient: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + AsyncRawAccountsClient + """ + return self._raw_client + + async def get_account_me(self, *, request_options: typing.Optional[RequestOptions] = None) -> AccountView: + """ + Get authenticated account information including credit balances and account details. + + Parameters + ---------- + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AccountView + Successful Response + + Examples + -------- + import asyncio + + from browser_use import AsyncBrowserUse + + client = AsyncBrowserUse( + api_key="YOUR_API_KEY", + ) + + + async def main() -> None: + await client.accounts.get_account_me() + + + asyncio.run(main()) + """ + _response = await self._raw_client.get_account_me(request_options=request_options) + return _response.data diff --git a/src/browser_use/accounts/raw_client.py b/src/browser_use/accounts/raw_client.py new file mode 100644 index 0000000..a391e4b --- /dev/null +++ b/src/browser_use/accounts/raw_client.py @@ -0,0 +1,114 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing +from json.decoder import JSONDecodeError + +from ..core.api_error import ApiError +from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from ..core.http_response import AsyncHttpResponse, HttpResponse +from ..core.request_options import RequestOptions +from ..core.unchecked_base_model import construct_type +from ..errors.not_found_error import NotFoundError +from ..types.account_view import AccountView + + +class RawAccountsClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._client_wrapper = client_wrapper + + def get_account_me(self, *, request_options: typing.Optional[RequestOptions] = None) -> HttpResponse[AccountView]: + """ + Get authenticated account information including credit balances and account details. + + Parameters + ---------- + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[AccountView] + Successful Response + """ + _response = self._client_wrapper.httpx_client.request( + "accounts/me", + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + AccountView, + construct_type( + type_=AccountView, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Optional[typing.Any], + construct_type( + type_=typing.Optional[typing.Any], # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + +class AsyncRawAccountsClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._client_wrapper = client_wrapper + + async def get_account_me( + self, *, request_options: typing.Optional[RequestOptions] = None + ) -> AsyncHttpResponse[AccountView]: + """ + Get authenticated account information including credit balances and account details. + + Parameters + ---------- + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[AccountView] + Successful Response + """ + _response = await self._client_wrapper.httpx_client.request( + "accounts/me", + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + AccountView, + construct_type( + type_=AccountView, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Optional[typing.Any], + construct_type( + type_=typing.Optional[typing.Any], # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) diff --git a/src/browser_use/base_client.py b/src/browser_use/base_client.py new file mode 100644 index 0000000..c0d4296 --- /dev/null +++ b/src/browser_use/base_client.py @@ -0,0 +1,165 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import httpx +from .accounts.client import AccountsClient, AsyncAccountsClient +from .core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from .environment import BrowserUseEnvironment +from .files.client import AsyncFilesClient, FilesClient +from .profiles.client import AsyncProfilesClient, ProfilesClient +from .sessions.client import AsyncSessionsClient, SessionsClient +from .tasks.client import AsyncTasksClient, TasksClient + + +class BaseClient: + """ + Use this class to access the different functions within the SDK. You can instantiate any number of clients with different configuration that will propagate to these functions. + + Parameters + ---------- + base_url : typing.Optional[str] + The base url to use for requests from the client. + + environment : BrowserUseEnvironment + The environment to use for requests from the client. from .environment import BrowserUseEnvironment + + + + Defaults to BrowserUseEnvironment.PRODUCTION + + + + api_key : str + headers : typing.Optional[typing.Dict[str, str]] + Additional headers to send with every request. + + timeout : typing.Optional[float] + The timeout to be used, in seconds, for requests. By default the timeout is 60 seconds, unless a custom httpx client is used, in which case this default is not enforced. + + follow_redirects : typing.Optional[bool] + Whether the default httpx client follows redirects or not, this is irrelevant if a custom httpx client is passed in. + + httpx_client : typing.Optional[httpx.Client] + The httpx client to use for making requests, a preconfigured client is used by default, however this is useful should you want to pass in any custom httpx configuration. + + Examples + -------- + from browser_use import BrowserUse + + client = BrowserUse( + api_key="YOUR_API_KEY", + ) + """ + + def __init__( + self, + *, + base_url: typing.Optional[str] = None, + environment: BrowserUseEnvironment = BrowserUseEnvironment.PRODUCTION, + api_key: str, + headers: typing.Optional[typing.Dict[str, str]] = None, + timeout: typing.Optional[float] = None, + follow_redirects: typing.Optional[bool] = True, + httpx_client: typing.Optional[httpx.Client] = None, + ): + _defaulted_timeout = ( + timeout if timeout is not None else 60 if httpx_client is None else httpx_client.timeout.read + ) + self._client_wrapper = SyncClientWrapper( + base_url=_get_base_url(base_url=base_url, environment=environment), + api_key=api_key, + headers=headers, + httpx_client=httpx_client + if httpx_client is not None + else httpx.Client(timeout=_defaulted_timeout, follow_redirects=follow_redirects) + if follow_redirects is not None + else httpx.Client(timeout=_defaulted_timeout), + timeout=_defaulted_timeout, + ) + self.accounts = AccountsClient(client_wrapper=self._client_wrapper) + self.tasks = TasksClient(client_wrapper=self._client_wrapper) + self.sessions = SessionsClient(client_wrapper=self._client_wrapper) + self.files = FilesClient(client_wrapper=self._client_wrapper) + self.profiles = ProfilesClient(client_wrapper=self._client_wrapper) + + +class AsyncBaseClient: + """ + Use this class to access the different functions within the SDK. You can instantiate any number of clients with different configuration that will propagate to these functions. + + Parameters + ---------- + base_url : typing.Optional[str] + The base url to use for requests from the client. + + environment : BrowserUseEnvironment + The environment to use for requests from the client. from .environment import BrowserUseEnvironment + + + + Defaults to BrowserUseEnvironment.PRODUCTION + + + + api_key : str + headers : typing.Optional[typing.Dict[str, str]] + Additional headers to send with every request. + + timeout : typing.Optional[float] + The timeout to be used, in seconds, for requests. By default the timeout is 60 seconds, unless a custom httpx client is used, in which case this default is not enforced. + + follow_redirects : typing.Optional[bool] + Whether the default httpx client follows redirects or not, this is irrelevant if a custom httpx client is passed in. + + httpx_client : typing.Optional[httpx.AsyncClient] + The httpx client to use for making requests, a preconfigured client is used by default, however this is useful should you want to pass in any custom httpx configuration. + + Examples + -------- + from browser_use import AsyncBrowserUse + + client = AsyncBrowserUse( + api_key="YOUR_API_KEY", + ) + """ + + def __init__( + self, + *, + base_url: typing.Optional[str] = None, + environment: BrowserUseEnvironment = BrowserUseEnvironment.PRODUCTION, + api_key: str, + headers: typing.Optional[typing.Dict[str, str]] = None, + timeout: typing.Optional[float] = None, + follow_redirects: typing.Optional[bool] = True, + httpx_client: typing.Optional[httpx.AsyncClient] = None, + ): + _defaulted_timeout = ( + timeout if timeout is not None else 60 if httpx_client is None else httpx_client.timeout.read + ) + self._client_wrapper = AsyncClientWrapper( + base_url=_get_base_url(base_url=base_url, environment=environment), + api_key=api_key, + headers=headers, + httpx_client=httpx_client + if httpx_client is not None + else httpx.AsyncClient(timeout=_defaulted_timeout, follow_redirects=follow_redirects) + if follow_redirects is not None + else httpx.AsyncClient(timeout=_defaulted_timeout), + timeout=_defaulted_timeout, + ) + self.accounts = AsyncAccountsClient(client_wrapper=self._client_wrapper) + self.tasks = AsyncTasksClient(client_wrapper=self._client_wrapper) + self.sessions = AsyncSessionsClient(client_wrapper=self._client_wrapper) + self.files = AsyncFilesClient(client_wrapper=self._client_wrapper) + self.profiles = AsyncProfilesClient(client_wrapper=self._client_wrapper) + + +def _get_base_url(*, base_url: typing.Optional[str] = None, environment: BrowserUseEnvironment) -> str: + if base_url is not None: + return base_url + elif environment is not None: + return environment.value + else: + raise Exception("Please pass in either base_url or environment to construct the client") diff --git a/src/browser_use/client.py b/src/browser_use/client.py new file mode 100644 index 0000000..e508239 --- /dev/null +++ b/src/browser_use/client.py @@ -0,0 +1,165 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import httpx +from .accounts.client import AccountsClient, AsyncAccountsClient +from .core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from .environment import BrowserUseEnvironment +from .files.client import AsyncFilesClient, FilesClient +from .profiles.client import AsyncProfilesClient, ProfilesClient +from .sessions.client import AsyncSessionsClient, SessionsClient +from .wrapper.tasks.client import AsyncBrowserUseTasksClient, BrowserUseTasksClient + + +class BrowserUse: + """ + Use this class to access the different functions within the SDK. You can instantiate any number of clients with different configuration that will propagate to these functions. + + Parameters + ---------- + base_url : typing.Optional[str] + The base url to use for requests from the client. + + environment : BrowserUseEnvironment + The environment to use for requests from the client. from .environment import BrowserUseEnvironment + + + + Defaults to BrowserUseEnvironment.PRODUCTION + + + + api_key : str + headers : typing.Optional[typing.Dict[str, str]] + Additional headers to send with every request. + + timeout : typing.Optional[float] + The timeout to be used, in seconds, for requests. By default the timeout is 60 seconds, unless a custom httpx client is used, in which case this default is not enforced. + + follow_redirects : typing.Optional[bool] + Whether the default httpx client follows redirects or not, this is irrelevant if a custom httpx client is passed in. + + httpx_client : typing.Optional[httpx.Client] + The httpx client to use for making requests, a preconfigured client is used by default, however this is useful should you want to pass in any custom httpx configuration. + + Examples + -------- + from browser_use import BrowserUse + + client = BrowserUse( + api_key="YOUR_API_KEY", + ) + """ + + def __init__( + self, + *, + base_url: typing.Optional[str] = None, + environment: BrowserUseEnvironment = BrowserUseEnvironment.PRODUCTION, + api_key: str, + headers: typing.Optional[typing.Dict[str, str]] = None, + timeout: typing.Optional[float] = None, + follow_redirects: typing.Optional[bool] = True, + httpx_client: typing.Optional[httpx.Client] = None, + ): + _defaulted_timeout = ( + timeout if timeout is not None else 60 if httpx_client is None else httpx_client.timeout.read + ) + self._client_wrapper = SyncClientWrapper( + base_url=_get_base_url(base_url=base_url, environment=environment), + api_key=api_key, + headers=headers, + httpx_client=httpx_client + if httpx_client is not None + else httpx.Client(timeout=_defaulted_timeout, follow_redirects=follow_redirects) + if follow_redirects is not None + else httpx.Client(timeout=_defaulted_timeout), + timeout=_defaulted_timeout, + ) + self.accounts = AccountsClient(client_wrapper=self._client_wrapper) + self.tasks = BrowserUseTasksClient(client_wrapper=self._client_wrapper) + self.sessions = SessionsClient(client_wrapper=self._client_wrapper) + self.files = FilesClient(client_wrapper=self._client_wrapper) + self.profiles = ProfilesClient(client_wrapper=self._client_wrapper) + + +class AsyncBrowserUse: + """ + Use this class to access the different functions within the SDK. You can instantiate any number of clients with different configuration that will propagate to these functions. + + Parameters + ---------- + base_url : typing.Optional[str] + The base url to use for requests from the client. + + environment : BrowserUseEnvironment + The environment to use for requests from the client. from .environment import BrowserUseEnvironment + + + + Defaults to BrowserUseEnvironment.PRODUCTION + + + + api_key : str + headers : typing.Optional[typing.Dict[str, str]] + Additional headers to send with every request. + + timeout : typing.Optional[float] + The timeout to be used, in seconds, for requests. By default the timeout is 60 seconds, unless a custom httpx client is used, in which case this default is not enforced. + + follow_redirects : typing.Optional[bool] + Whether the default httpx client follows redirects or not, this is irrelevant if a custom httpx client is passed in. + + httpx_client : typing.Optional[httpx.AsyncClient] + The httpx client to use for making requests, a preconfigured client is used by default, however this is useful should you want to pass in any custom httpx configuration. + + Examples + -------- + from browser_use import AsyncBrowserUse + + client = AsyncBrowserUse( + api_key="YOUR_API_KEY", + ) + """ + + def __init__( + self, + *, + base_url: typing.Optional[str] = None, + environment: BrowserUseEnvironment = BrowserUseEnvironment.PRODUCTION, + api_key: str, + headers: typing.Optional[typing.Dict[str, str]] = None, + timeout: typing.Optional[float] = None, + follow_redirects: typing.Optional[bool] = True, + httpx_client: typing.Optional[httpx.AsyncClient] = None, + ): + _defaulted_timeout = ( + timeout if timeout is not None else 60 if httpx_client is None else httpx_client.timeout.read + ) + self._client_wrapper = AsyncClientWrapper( + base_url=_get_base_url(base_url=base_url, environment=environment), + api_key=api_key, + headers=headers, + httpx_client=httpx_client + if httpx_client is not None + else httpx.AsyncClient(timeout=_defaulted_timeout, follow_redirects=follow_redirects) + if follow_redirects is not None + else httpx.AsyncClient(timeout=_defaulted_timeout), + timeout=_defaulted_timeout, + ) + self.accounts = AsyncAccountsClient(client_wrapper=self._client_wrapper) + self.tasks = AsyncBrowserUseTasksClient(client_wrapper=self._client_wrapper) + self.sessions = AsyncSessionsClient(client_wrapper=self._client_wrapper) + self.files = AsyncFilesClient(client_wrapper=self._client_wrapper) + self.profiles = AsyncProfilesClient(client_wrapper=self._client_wrapper) + + +def _get_base_url(*, base_url: typing.Optional[str] = None, environment: BrowserUseEnvironment) -> str: + if base_url is not None: + return base_url + elif environment is not None: + return environment.value + else: + raise Exception("Please pass in either base_url or environment to construct the client") diff --git a/src/browser_use/core/__init__.py b/src/browser_use/core/__init__.py new file mode 100644 index 0000000..73955ba --- /dev/null +++ b/src/browser_use/core/__init__.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +from .file import File, with_content_type + +__all__ = ["File", "with_content_type"] diff --git a/src/browser_use/core/api_error.py b/src/browser_use/core/api_error.py new file mode 100644 index 0000000..6f850a6 --- /dev/null +++ b/src/browser_use/core/api_error.py @@ -0,0 +1,23 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import Any, Dict, Optional + + +class ApiError(Exception): + headers: Optional[Dict[str, str]] + status_code: Optional[int] + body: Any + + def __init__( + self, + *, + headers: Optional[Dict[str, str]] = None, + status_code: Optional[int] = None, + body: Any = None, + ) -> None: + self.headers = headers + self.status_code = status_code + self.body = body + + def __str__(self) -> str: + return f"headers: {self.headers}, status_code: {self.status_code}, body: {self.body}" diff --git a/src/browser_use/core/client_wrapper.py b/src/browser_use/core/client_wrapper.py new file mode 100644 index 0000000..3cb5004 --- /dev/null +++ b/src/browser_use/core/client_wrapper.py @@ -0,0 +1,78 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import httpx +from .http_client import AsyncHttpClient, HttpClient + + +class BaseClientWrapper: + def __init__( + self, + *, + api_key: str, + headers: typing.Optional[typing.Dict[str, str]] = None, + base_url: str, + timeout: typing.Optional[float] = None, + ): + self.api_key = api_key + self._headers = headers + self._base_url = base_url + self._timeout = timeout + + def get_headers(self) -> typing.Dict[str, str]: + headers: typing.Dict[str, str] = { + "X-Fern-Language": "Python", + "X-Fern-SDK-Name": "browser-use", + "X-Fern-SDK-Version": "0.0.0", + **(self.get_custom_headers() or {}), + } + headers["X-Browser-Use-API-Key"] = self.api_key + return headers + + def get_custom_headers(self) -> typing.Optional[typing.Dict[str, str]]: + return self._headers + + def get_base_url(self) -> str: + return self._base_url + + def get_timeout(self) -> typing.Optional[float]: + return self._timeout + + +class SyncClientWrapper(BaseClientWrapper): + def __init__( + self, + *, + api_key: str, + headers: typing.Optional[typing.Dict[str, str]] = None, + base_url: str, + timeout: typing.Optional[float] = None, + httpx_client: httpx.Client, + ): + super().__init__(api_key=api_key, headers=headers, base_url=base_url, timeout=timeout) + self.httpx_client = HttpClient( + httpx_client=httpx_client, + base_headers=self.get_headers, + base_timeout=self.get_timeout, + base_url=self.get_base_url, + ) + + +class AsyncClientWrapper(BaseClientWrapper): + def __init__( + self, + *, + api_key: str, + headers: typing.Optional[typing.Dict[str, str]] = None, + base_url: str, + timeout: typing.Optional[float] = None, + httpx_client: httpx.AsyncClient, + ): + super().__init__(api_key=api_key, headers=headers, base_url=base_url, timeout=timeout) + self.httpx_client = AsyncHttpClient( + httpx_client=httpx_client, + base_headers=self.get_headers, + base_timeout=self.get_timeout, + base_url=self.get_base_url, + ) diff --git a/src/browser_use/core/datetime_utils.py b/src/browser_use/core/datetime_utils.py new file mode 100644 index 0000000..7c9864a --- /dev/null +++ b/src/browser_use/core/datetime_utils.py @@ -0,0 +1,28 @@ +# This file was auto-generated by Fern from our API Definition. + +import datetime as dt + + +def serialize_datetime(v: dt.datetime) -> str: + """ + Serialize a datetime including timezone info. + + Uses the timezone info provided if present, otherwise uses the current runtime's timezone info. + + UTC datetimes end in "Z" while all other timezones are represented as offset from UTC, e.g. +05:00. + """ + + def _serialize_zoned_datetime(v: dt.datetime) -> str: + if v.tzinfo is not None and v.tzinfo.tzname(None) == dt.timezone.utc.tzname(None): + # UTC is a special case where we use "Z" at the end instead of "+00:00" + return v.isoformat().replace("+00:00", "Z") + else: + # Delegate to the typical +/- offset format + return v.isoformat() + + if v.tzinfo is not None: + return _serialize_zoned_datetime(v) + else: + local_tz = dt.datetime.now().astimezone().tzinfo + localized_dt = v.replace(tzinfo=local_tz) + return _serialize_zoned_datetime(localized_dt) diff --git a/src/browser_use/core/file.py b/src/browser_use/core/file.py new file mode 100644 index 0000000..44b0d27 --- /dev/null +++ b/src/browser_use/core/file.py @@ -0,0 +1,67 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import IO, Dict, List, Mapping, Optional, Tuple, Union, cast + +# File typing inspired by the flexibility of types within the httpx library +# https://github.com/encode/httpx/blob/master/httpx/_types.py +FileContent = Union[IO[bytes], bytes, str] +File = 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], + ], +] + + +def convert_file_dict_to_httpx_tuples( + d: Dict[str, Union[File, List[File]]], +) -> List[Tuple[str, File]]: + """ + The format we use is a list of tuples, where the first element is the + name of the file and the second is the file object. Typically HTTPX wants + a dict, but to be able to send lists of files, you have to use the list + approach (which also works for non-lists) + https://github.com/encode/httpx/pull/1032 + """ + + httpx_tuples = [] + for key, file_like in d.items(): + if isinstance(file_like, list): + for file_like_item in file_like: + httpx_tuples.append((key, file_like_item)) + else: + httpx_tuples.append((key, file_like)) + return httpx_tuples + + +def with_content_type(*, file: File, default_content_type: str) -> File: + """ + This function resolves to the file's content type, if provided, and defaults + to the default_content_type value if not. + """ + if isinstance(file, tuple): + if len(file) == 2: + filename, content = cast(Tuple[Optional[str], FileContent], file) # type: ignore + return (filename, content, default_content_type) + elif len(file) == 3: + filename, content, file_content_type = cast(Tuple[Optional[str], FileContent, Optional[str]], file) # type: ignore + out_content_type = file_content_type or default_content_type + return (filename, content, out_content_type) + elif len(file) == 4: + filename, content, file_content_type, headers = cast( # type: ignore + Tuple[Optional[str], FileContent, Optional[str], Mapping[str, str]], file + ) + out_content_type = file_content_type or default_content_type + return (filename, content, out_content_type, headers) + else: + raise ValueError(f"Unexpected tuple length: {len(file)}") + return (None, file, default_content_type) diff --git a/src/browser_use/core/force_multipart.py b/src/browser_use/core/force_multipart.py new file mode 100644 index 0000000..ae24ccf --- /dev/null +++ b/src/browser_use/core/force_multipart.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + + +class ForceMultipartDict(dict): + """ + A dictionary subclass that always evaluates to True in boolean contexts. + + This is used to force multipart/form-data encoding in HTTP requests even when + the dictionary is empty, which would normally evaluate to False. + """ + + def __bool__(self): + return True + + +FORCE_MULTIPART = ForceMultipartDict() diff --git a/src/browser_use/core/http_client.py b/src/browser_use/core/http_client.py new file mode 100644 index 0000000..e4173f9 --- /dev/null +++ b/src/browser_use/core/http_client.py @@ -0,0 +1,543 @@ +# This file was auto-generated by Fern from our API Definition. + +import asyncio +import email.utils +import re +import time +import typing +import urllib.parse +from contextlib import asynccontextmanager, contextmanager +from random import random + +import httpx +from .file import File, convert_file_dict_to_httpx_tuples +from .force_multipart import FORCE_MULTIPART +from .jsonable_encoder import jsonable_encoder +from .query_encoder import encode_query +from .remove_none_from_dict import remove_none_from_dict +from .request_options import RequestOptions +from httpx._types import RequestFiles + +INITIAL_RETRY_DELAY_SECONDS = 0.5 +MAX_RETRY_DELAY_SECONDS = 10 +MAX_RETRY_DELAY_SECONDS_FROM_HEADER = 30 + + +def _parse_retry_after(response_headers: httpx.Headers) -> typing.Optional[float]: + """ + This function parses the `Retry-After` header in a HTTP response and returns the number of seconds to wait. + + Inspired by the urllib3 retry implementation. + """ + retry_after_ms = response_headers.get("retry-after-ms") + if retry_after_ms is not None: + try: + return int(retry_after_ms) / 1000 if retry_after_ms > 0 else 0 + except Exception: + pass + + retry_after = response_headers.get("retry-after") + if retry_after is None: + return None + + # Attempt to parse the header as an int. + if re.match(r"^\s*[0-9]+\s*$", retry_after): + seconds = float(retry_after) + # Fallback to parsing it as a date. + else: + retry_date_tuple = email.utils.parsedate_tz(retry_after) + if retry_date_tuple is None: + return None + if retry_date_tuple[9] is None: # Python 2 + # Assume UTC if no timezone was specified + # On Python2.7, parsedate_tz returns None for a timezone offset + # instead of 0 if no timezone is given, where mktime_tz treats + # a None timezone offset as local time. + retry_date_tuple = retry_date_tuple[:9] + (0,) + retry_date_tuple[10:] + + retry_date = email.utils.mktime_tz(retry_date_tuple) + seconds = retry_date - time.time() + + if seconds < 0: + seconds = 0 + + return seconds + + +def _retry_timeout(response: httpx.Response, retries: int) -> float: + """ + Determine the amount of time to wait before retrying a request. + This function begins by trying to parse a retry-after header from the response, and then proceeds to use exponential backoff + with a jitter to determine the number of seconds to wait. + """ + + # 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 = _parse_retry_after(response.headers) + if retry_after is not None and retry_after <= MAX_RETRY_DELAY_SECONDS_FROM_HEADER: + return retry_after + + # Apply exponential backoff, capped at MAX_RETRY_DELAY_SECONDS. + retry_delay = min(INITIAL_RETRY_DELAY_SECONDS * pow(2.0, retries), MAX_RETRY_DELAY_SECONDS) + + # Add a randomness / jitter to the retry delay to avoid overwhelming the server with retries. + timeout = retry_delay * (1 - 0.25 * random()) + return timeout if timeout >= 0 else 0 + + +def _should_retry(response: httpx.Response) -> bool: + retryable_400s = [429, 408, 409] + return response.status_code >= 500 or response.status_code in retryable_400s + + +def remove_omit_from_dict( + original: typing.Dict[str, typing.Optional[typing.Any]], + omit: typing.Optional[typing.Any], +) -> typing.Dict[str, typing.Any]: + if omit is None: + return original + new: typing.Dict[str, typing.Any] = {} + for key, value in original.items(): + if value is not omit: + new[key] = value + return new + + +def maybe_filter_request_body( + data: typing.Optional[typing.Any], + request_options: typing.Optional[RequestOptions], + omit: typing.Optional[typing.Any], +) -> typing.Optional[typing.Any]: + if data is None: + return ( + jsonable_encoder(request_options.get("additional_body_parameters", {})) or {} + if request_options is not None + else None + ) + elif not isinstance(data, typing.Mapping): + data_content = jsonable_encoder(data) + else: + data_content = { + **(jsonable_encoder(remove_omit_from_dict(data, omit))), # type: ignore + **( + jsonable_encoder(request_options.get("additional_body_parameters", {})) or {} + if request_options is not None + else {} + ), + } + return data_content + + +# Abstracted out for testing purposes +def get_request_body( + *, + json: typing.Optional[typing.Any], + data: typing.Optional[typing.Any], + request_options: typing.Optional[RequestOptions], + omit: typing.Optional[typing.Any], +) -> typing.Tuple[typing.Optional[typing.Any], typing.Optional[typing.Any]]: + json_body = None + data_body = None + if data is not None: + data_body = maybe_filter_request_body(data, request_options, omit) + else: + # If both data and json are None, we send json data in the event extra properties are specified + json_body = maybe_filter_request_body(json, request_options, omit) + + # If you have an empty JSON body, you should just send None + return (json_body if json_body != {} else None), data_body if data_body != {} else None + + +class HttpClient: + def __init__( + self, + *, + httpx_client: httpx.Client, + base_timeout: typing.Callable[[], typing.Optional[float]], + base_headers: typing.Callable[[], typing.Dict[str, str]], + base_url: typing.Optional[typing.Callable[[], str]] = None, + ): + self.base_url = base_url + self.base_timeout = base_timeout + self.base_headers = base_headers + self.httpx_client = httpx_client + + def get_base_url(self, maybe_base_url: typing.Optional[str]) -> str: + base_url = maybe_base_url + if self.base_url is not None and base_url is None: + base_url = self.base_url() + + if base_url is None: + raise ValueError("A base_url is required to make this request, please provide one and try again.") + return base_url + + def request( + self, + path: typing.Optional[str] = None, + *, + method: str, + base_url: typing.Optional[str] = None, + params: typing.Optional[typing.Dict[str, typing.Any]] = None, + json: typing.Optional[typing.Any] = None, + data: typing.Optional[typing.Any] = None, + content: typing.Optional[typing.Union[bytes, typing.Iterator[bytes], typing.AsyncIterator[bytes]]] = None, + files: typing.Optional[ + typing.Union[ + typing.Dict[str, typing.Optional[typing.Union[File, typing.List[File]]]], + typing.List[typing.Tuple[str, File]], + ] + ] = None, + headers: typing.Optional[typing.Dict[str, typing.Any]] = None, + request_options: typing.Optional[RequestOptions] = None, + retries: int = 2, + omit: typing.Optional[typing.Any] = None, + force_multipart: typing.Optional[bool] = None, + ) -> httpx.Response: + base_url = self.get_base_url(base_url) + timeout = ( + request_options.get("timeout_in_seconds") + if request_options is not None and request_options.get("timeout_in_seconds") is not None + else self.base_timeout() + ) + + json_body, data_body = get_request_body(json=json, data=data, request_options=request_options, omit=omit) + + request_files: typing.Optional[RequestFiles] = ( + convert_file_dict_to_httpx_tuples(remove_omit_from_dict(remove_none_from_dict(files), omit)) + if (files is not None and files is not omit and isinstance(files, dict)) + else None + ) + + if (request_files is None or len(request_files) == 0) and force_multipart: + request_files = FORCE_MULTIPART + + response = self.httpx_client.request( + method=method, + url=urllib.parse.urljoin(f"{base_url}/", path), + headers=jsonable_encoder( + remove_none_from_dict( + { + **self.base_headers(), + **(headers if headers is not None else {}), + **(request_options.get("additional_headers", {}) or {} if request_options is not None else {}), + } + ) + ), + params=encode_query( + jsonable_encoder( + remove_none_from_dict( + remove_omit_from_dict( + { + **(params if params is not None else {}), + **( + request_options.get("additional_query_parameters", {}) or {} + if request_options is not None + else {} + ), + }, + omit, + ) + ) + ) + ), + json=json_body, + data=data_body, + content=content, + files=request_files, + timeout=timeout, + ) + + max_retries: int = request_options.get("max_retries", 0) if request_options is not None else 0 + if _should_retry(response=response): + if max_retries > retries: + time.sleep(_retry_timeout(response=response, retries=retries)) + return self.request( + path=path, + method=method, + base_url=base_url, + params=params, + json=json, + content=content, + files=files, + headers=headers, + request_options=request_options, + retries=retries + 1, + omit=omit, + ) + + return response + + @contextmanager + def stream( + self, + path: typing.Optional[str] = None, + *, + method: str, + base_url: typing.Optional[str] = None, + params: typing.Optional[typing.Dict[str, typing.Any]] = None, + json: typing.Optional[typing.Any] = None, + data: typing.Optional[typing.Any] = None, + content: typing.Optional[typing.Union[bytes, typing.Iterator[bytes], typing.AsyncIterator[bytes]]] = None, + files: typing.Optional[ + typing.Union[ + typing.Dict[str, typing.Optional[typing.Union[File, typing.List[File]]]], + typing.List[typing.Tuple[str, File]], + ] + ] = None, + headers: typing.Optional[typing.Dict[str, typing.Any]] = None, + request_options: typing.Optional[RequestOptions] = None, + retries: int = 2, + omit: typing.Optional[typing.Any] = None, + force_multipart: typing.Optional[bool] = None, + ) -> typing.Iterator[httpx.Response]: + base_url = self.get_base_url(base_url) + timeout = ( + request_options.get("timeout_in_seconds") + if request_options is not None and request_options.get("timeout_in_seconds") is not None + else self.base_timeout() + ) + + request_files: typing.Optional[RequestFiles] = ( + convert_file_dict_to_httpx_tuples(remove_omit_from_dict(remove_none_from_dict(files), omit)) + if (files is not None and files is not omit and isinstance(files, dict)) + else None + ) + + if (request_files is None or len(request_files) == 0) and force_multipart: + request_files = FORCE_MULTIPART + + json_body, data_body = get_request_body(json=json, data=data, request_options=request_options, omit=omit) + + with self.httpx_client.stream( + method=method, + url=urllib.parse.urljoin(f"{base_url}/", path), + headers=jsonable_encoder( + remove_none_from_dict( + { + **self.base_headers(), + **(headers if headers is not None else {}), + **(request_options.get("additional_headers", {}) if request_options is not None else {}), + } + ) + ), + params=encode_query( + jsonable_encoder( + remove_none_from_dict( + remove_omit_from_dict( + { + **(params if params is not None else {}), + **( + request_options.get("additional_query_parameters", {}) + if request_options is not None + else {} + ), + }, + omit, + ) + ) + ) + ), + json=json_body, + data=data_body, + content=content, + files=request_files, + timeout=timeout, + ) as stream: + yield stream + + +class AsyncHttpClient: + def __init__( + self, + *, + httpx_client: httpx.AsyncClient, + base_timeout: typing.Callable[[], typing.Optional[float]], + base_headers: typing.Callable[[], typing.Dict[str, str]], + base_url: typing.Optional[typing.Callable[[], str]] = None, + ): + self.base_url = base_url + self.base_timeout = base_timeout + self.base_headers = base_headers + self.httpx_client = httpx_client + + def get_base_url(self, maybe_base_url: typing.Optional[str]) -> str: + base_url = maybe_base_url + if self.base_url is not None and base_url is None: + base_url = self.base_url() + + if base_url is None: + raise ValueError("A base_url is required to make this request, please provide one and try again.") + return base_url + + async def request( + self, + path: typing.Optional[str] = None, + *, + method: str, + base_url: typing.Optional[str] = None, + params: typing.Optional[typing.Dict[str, typing.Any]] = None, + json: typing.Optional[typing.Any] = None, + data: typing.Optional[typing.Any] = None, + content: typing.Optional[typing.Union[bytes, typing.Iterator[bytes], typing.AsyncIterator[bytes]]] = None, + files: typing.Optional[ + typing.Union[ + typing.Dict[str, typing.Optional[typing.Union[File, typing.List[File]]]], + typing.List[typing.Tuple[str, File]], + ] + ] = None, + headers: typing.Optional[typing.Dict[str, typing.Any]] = None, + request_options: typing.Optional[RequestOptions] = None, + retries: int = 2, + omit: typing.Optional[typing.Any] = None, + force_multipart: typing.Optional[bool] = None, + ) -> httpx.Response: + base_url = self.get_base_url(base_url) + timeout = ( + request_options.get("timeout_in_seconds") + if request_options is not None and request_options.get("timeout_in_seconds") is not None + else self.base_timeout() + ) + + request_files: typing.Optional[RequestFiles] = ( + convert_file_dict_to_httpx_tuples(remove_omit_from_dict(remove_none_from_dict(files), omit)) + if (files is not None and files is not omit and isinstance(files, dict)) + else None + ) + + if (request_files is None or len(request_files) == 0) and force_multipart: + request_files = FORCE_MULTIPART + + json_body, data_body = get_request_body(json=json, data=data, request_options=request_options, omit=omit) + + # Add the input to each of these and do None-safety checks + response = await self.httpx_client.request( + method=method, + url=urllib.parse.urljoin(f"{base_url}/", path), + headers=jsonable_encoder( + remove_none_from_dict( + { + **self.base_headers(), + **(headers if headers is not None else {}), + **(request_options.get("additional_headers", {}) or {} if request_options is not None else {}), + } + ) + ), + params=encode_query( + jsonable_encoder( + remove_none_from_dict( + remove_omit_from_dict( + { + **(params if params is not None else {}), + **( + request_options.get("additional_query_parameters", {}) or {} + if request_options is not None + else {} + ), + }, + omit, + ) + ) + ) + ), + json=json_body, + data=data_body, + content=content, + files=request_files, + timeout=timeout, + ) + + max_retries: int = request_options.get("max_retries", 0) if request_options is not None else 0 + if _should_retry(response=response): + if max_retries > retries: + await asyncio.sleep(_retry_timeout(response=response, retries=retries)) + return await self.request( + path=path, + method=method, + base_url=base_url, + params=params, + json=json, + content=content, + files=files, + headers=headers, + request_options=request_options, + retries=retries + 1, + omit=omit, + ) + return response + + @asynccontextmanager + async def stream( + self, + path: typing.Optional[str] = None, + *, + method: str, + base_url: typing.Optional[str] = None, + params: typing.Optional[typing.Dict[str, typing.Any]] = None, + json: typing.Optional[typing.Any] = None, + data: typing.Optional[typing.Any] = None, + content: typing.Optional[typing.Union[bytes, typing.Iterator[bytes], typing.AsyncIterator[bytes]]] = None, + files: typing.Optional[ + typing.Union[ + typing.Dict[str, typing.Optional[typing.Union[File, typing.List[File]]]], + typing.List[typing.Tuple[str, File]], + ] + ] = None, + headers: typing.Optional[typing.Dict[str, typing.Any]] = None, + request_options: typing.Optional[RequestOptions] = None, + retries: int = 2, + omit: typing.Optional[typing.Any] = None, + force_multipart: typing.Optional[bool] = None, + ) -> typing.AsyncIterator[httpx.Response]: + base_url = self.get_base_url(base_url) + timeout = ( + request_options.get("timeout_in_seconds") + if request_options is not None and request_options.get("timeout_in_seconds") is not None + else self.base_timeout() + ) + + request_files: typing.Optional[RequestFiles] = ( + convert_file_dict_to_httpx_tuples(remove_omit_from_dict(remove_none_from_dict(files), omit)) + if (files is not None and files is not omit and isinstance(files, dict)) + else None + ) + + if (request_files is None or len(request_files) == 0) and force_multipart: + request_files = FORCE_MULTIPART + + json_body, data_body = get_request_body(json=json, data=data, request_options=request_options, omit=omit) + + async with self.httpx_client.stream( + method=method, + url=urllib.parse.urljoin(f"{base_url}/", path), + headers=jsonable_encoder( + remove_none_from_dict( + { + **self.base_headers(), + **(headers if headers is not None else {}), + **(request_options.get("additional_headers", {}) if request_options is not None else {}), + } + ) + ), + params=encode_query( + jsonable_encoder( + remove_none_from_dict( + remove_omit_from_dict( + { + **(params if params is not None else {}), + **( + request_options.get("additional_query_parameters", {}) + if request_options is not None + else {} + ), + }, + omit=omit, + ) + ) + ) + ), + json=json_body, + data=data_body, + content=content, + files=request_files, + timeout=timeout, + ) as stream: + yield stream diff --git a/src/browser_use/core/http_response.py b/src/browser_use/core/http_response.py new file mode 100644 index 0000000..48a1798 --- /dev/null +++ b/src/browser_use/core/http_response.py @@ -0,0 +1,55 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import Dict, Generic, TypeVar + +import httpx + +T = TypeVar("T") +"""Generic to represent the underlying type of the data wrapped by the HTTP response.""" + + +class BaseHttpResponse: + """Minimalist HTTP response wrapper that exposes response headers.""" + + _response: httpx.Response + + def __init__(self, response: httpx.Response): + self._response = response + + @property + def headers(self) -> Dict[str, str]: + return dict(self._response.headers) + + +class HttpResponse(Generic[T], BaseHttpResponse): + """HTTP response wrapper that exposes response headers and data.""" + + _data: T + + def __init__(self, response: httpx.Response, data: T): + super().__init__(response) + self._data = data + + @property + def data(self) -> T: + return self._data + + def close(self) -> None: + self._response.close() + + +class AsyncHttpResponse(Generic[T], BaseHttpResponse): + """HTTP response wrapper that exposes response headers and data.""" + + _data: T + + def __init__(self, response: httpx.Response, data: T): + super().__init__(response) + self._data = data + + @property + def data(self) -> T: + return self._data + + async def close(self) -> None: + await self._response.aclose() diff --git a/src/browser_use/core/jsonable_encoder.py b/src/browser_use/core/jsonable_encoder.py new file mode 100644 index 0000000..afee366 --- /dev/null +++ b/src/browser_use/core/jsonable_encoder.py @@ -0,0 +1,100 @@ +# This file was auto-generated by Fern from our API Definition. + +""" +jsonable_encoder converts a Python object to a JSON-friendly dict +(e.g. datetimes to strings, Pydantic models to dicts). + +Taken from FastAPI, and made a bit simpler +https://github.com/tiangolo/fastapi/blob/master/fastapi/encoders.py +""" + +import base64 +import dataclasses +import datetime as dt +from enum import Enum +from pathlib import PurePath +from types import GeneratorType +from typing import Any, Callable, Dict, List, Optional, Set, Union + +import pydantic +from .datetime_utils import serialize_datetime +from .pydantic_utilities import ( + IS_PYDANTIC_V2, + encode_by_type, + to_jsonable_with_fallback, +) + +SetIntStr = Set[Union[int, str]] +DictIntStrAny = Dict[Union[int, str], Any] + + +def jsonable_encoder(obj: Any, custom_encoder: Optional[Dict[Any, Callable[[Any], Any]]] = None) -> Any: + custom_encoder = custom_encoder or {} + if custom_encoder: + if type(obj) in custom_encoder: + return custom_encoder[type(obj)](obj) + else: + for encoder_type, encoder_instance in custom_encoder.items(): + if isinstance(obj, encoder_type): + return encoder_instance(obj) + if isinstance(obj, pydantic.BaseModel): + if IS_PYDANTIC_V2: + encoder = getattr(obj.model_config, "json_encoders", {}) # type: ignore # Pydantic v2 + else: + encoder = getattr(obj.__config__, "json_encoders", {}) # type: ignore # Pydantic v1 + if custom_encoder: + encoder.update(custom_encoder) + obj_dict = obj.dict(by_alias=True) + if "__root__" in obj_dict: + obj_dict = obj_dict["__root__"] + if "root" in obj_dict: + obj_dict = obj_dict["root"] + return jsonable_encoder(obj_dict, custom_encoder=encoder) + if dataclasses.is_dataclass(obj): + obj_dict = dataclasses.asdict(obj) # type: ignore + return jsonable_encoder(obj_dict, custom_encoder=custom_encoder) + if isinstance(obj, bytes): + return base64.b64encode(obj).decode("utf-8") + if isinstance(obj, Enum): + return obj.value + if isinstance(obj, PurePath): + return str(obj) + if isinstance(obj, (str, int, float, type(None))): + return obj + if isinstance(obj, dt.datetime): + return serialize_datetime(obj) + if isinstance(obj, dt.date): + return str(obj) + if isinstance(obj, dict): + encoded_dict = {} + allowed_keys = set(obj.keys()) + for key, value in obj.items(): + if key in allowed_keys: + encoded_key = jsonable_encoder(key, custom_encoder=custom_encoder) + encoded_value = jsonable_encoder(value, custom_encoder=custom_encoder) + encoded_dict[encoded_key] = encoded_value + return encoded_dict + if isinstance(obj, (list, set, frozenset, GeneratorType, tuple)): + encoded_list = [] + for item in obj: + encoded_list.append(jsonable_encoder(item, custom_encoder=custom_encoder)) + return encoded_list + + def fallback_serializer(o: Any) -> Any: + attempt_encode = encode_by_type(o) + if attempt_encode is not None: + return attempt_encode + + try: + data = dict(o) + except Exception as e: + errors: List[Exception] = [] + errors.append(e) + try: + data = vars(o) + except Exception as e: + errors.append(e) + raise ValueError(errors) from e + return jsonable_encoder(data, custom_encoder=custom_encoder) + + return to_jsonable_with_fallback(obj, fallback_serializer) diff --git a/src/browser_use/core/pydantic_utilities.py b/src/browser_use/core/pydantic_utilities.py new file mode 100644 index 0000000..7db2950 --- /dev/null +++ b/src/browser_use/core/pydantic_utilities.py @@ -0,0 +1,255 @@ +# This file was auto-generated by Fern from our API Definition. + +# nopycln: file +import datetime as dt +from collections import defaultdict +from typing import Any, Callable, ClassVar, Dict, List, Mapping, Optional, Set, Tuple, Type, TypeVar, Union, cast + +import pydantic + +IS_PYDANTIC_V2 = pydantic.VERSION.startswith("2.") + +if IS_PYDANTIC_V2: + from pydantic.v1.datetime_parse import parse_date as parse_date + from pydantic.v1.datetime_parse import parse_datetime as parse_datetime + from pydantic.v1.fields import ModelField as ModelField + from pydantic.v1.json import ENCODERS_BY_TYPE as encoders_by_type # type: ignore[attr-defined] + from pydantic.v1.typing import get_args as get_args + from pydantic.v1.typing import get_origin as get_origin + from pydantic.v1.typing import is_literal_type as is_literal_type + from pydantic.v1.typing import is_union as is_union +else: + from pydantic.datetime_parse import parse_date as parse_date # type: ignore[no-redef] + from pydantic.datetime_parse import parse_datetime as parse_datetime # type: ignore[no-redef] + from pydantic.fields import ModelField as ModelField # type: ignore[attr-defined, no-redef] + from pydantic.json import ENCODERS_BY_TYPE as encoders_by_type # type: ignore[no-redef] + from pydantic.typing import get_args as get_args # type: ignore[no-redef] + from pydantic.typing import get_origin as get_origin # type: ignore[no-redef] + from pydantic.typing import is_literal_type as is_literal_type # type: ignore[no-redef] + from pydantic.typing import is_union as is_union # type: ignore[no-redef] + +from .datetime_utils import serialize_datetime +from .serialization import convert_and_respect_annotation_metadata +from typing_extensions import TypeAlias + +T = TypeVar("T") +Model = TypeVar("Model", bound=pydantic.BaseModel) + + +def parse_obj_as(type_: Type[T], object_: Any) -> T: + dealiased_object = convert_and_respect_annotation_metadata(object_=object_, annotation=type_, direction="read") + if IS_PYDANTIC_V2: + adapter = pydantic.TypeAdapter(type_) # type: ignore[attr-defined] + return adapter.validate_python(dealiased_object) + return pydantic.parse_obj_as(type_, dealiased_object) + + +def to_jsonable_with_fallback(obj: Any, fallback_serializer: Callable[[Any], Any]) -> Any: + if IS_PYDANTIC_V2: + from pydantic_core import to_jsonable_python + + return to_jsonable_python(obj, fallback=fallback_serializer) + return fallback_serializer(obj) + + +class UniversalBaseModel(pydantic.BaseModel): + if IS_PYDANTIC_V2: + model_config: ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( # type: ignore[typeddict-unknown-key] + # Allow fields beginning with `model_` to be used in the model + protected_namespaces=(), + ) + + @pydantic.model_serializer(mode="plain", when_used="json") # type: ignore[attr-defined] + def serialize_model(self) -> Any: # type: ignore[name-defined] + serialized = self.model_dump() + data = {k: serialize_datetime(v) if isinstance(v, dt.datetime) else v for k, v in serialized.items()} + return data + + else: + + class Config: + smart_union = True + json_encoders = {dt.datetime: serialize_datetime} + + @classmethod + def model_construct(cls: Type["Model"], _fields_set: Optional[Set[str]] = None, **values: Any) -> "Model": + dealiased_object = convert_and_respect_annotation_metadata(object_=values, annotation=cls, direction="read") + return cls.construct(_fields_set, **dealiased_object) + + @classmethod + def construct(cls: Type["Model"], _fields_set: Optional[Set[str]] = None, **values: Any) -> "Model": + dealiased_object = convert_and_respect_annotation_metadata(object_=values, annotation=cls, direction="read") + if IS_PYDANTIC_V2: + return super().model_construct(_fields_set, **dealiased_object) # type: ignore[misc] + return super().construct(_fields_set, **dealiased_object) + + def json(self, **kwargs: Any) -> str: + kwargs_with_defaults = { + "by_alias": True, + "exclude_unset": True, + **kwargs, + } + if IS_PYDANTIC_V2: + return super().model_dump_json(**kwargs_with_defaults) # type: ignore[misc] + return super().json(**kwargs_with_defaults) + + def dict(self, **kwargs: Any) -> Dict[str, Any]: + """ + Override the default dict method to `exclude_unset` by default. This function patches + `exclude_unset` to work include fields within non-None default values. + """ + # Note: the logic here is multiplexed given the levers exposed in Pydantic V1 vs V2 + # Pydantic V1's .dict can be extremely slow, so we do not want to call it twice. + # + # We'd ideally do the same for Pydantic V2, but it shells out to a library to serialize models + # that we have less control over, and this is less intrusive than custom serializers for now. + if IS_PYDANTIC_V2: + kwargs_with_defaults_exclude_unset = { + **kwargs, + "by_alias": True, + "exclude_unset": True, + "exclude_none": False, + } + kwargs_with_defaults_exclude_none = { + **kwargs, + "by_alias": True, + "exclude_none": True, + "exclude_unset": False, + } + dict_dump = deep_union_pydantic_dicts( + super().model_dump(**kwargs_with_defaults_exclude_unset), # type: ignore[misc] + super().model_dump(**kwargs_with_defaults_exclude_none), # type: ignore[misc] + ) + + else: + _fields_set = self.__fields_set__.copy() + + fields = _get_model_fields(self.__class__) + for name, field in fields.items(): + if name not in _fields_set: + default = _get_field_default(field) + + # If the default values are non-null act like they've been set + # This effectively allows exclude_unset to work like exclude_none where + # the latter passes through intentionally set none values. + if default is not None or ("exclude_unset" in kwargs and not kwargs["exclude_unset"]): + _fields_set.add(name) + + if default is not None: + self.__fields_set__.add(name) + + kwargs_with_defaults_exclude_unset_include_fields = { + "by_alias": True, + "exclude_unset": True, + "include": _fields_set, + **kwargs, + } + + dict_dump = super().dict(**kwargs_with_defaults_exclude_unset_include_fields) + + return convert_and_respect_annotation_metadata(object_=dict_dump, annotation=self.__class__, direction="write") + + +def _union_list_of_pydantic_dicts(source: List[Any], destination: List[Any]) -> List[Any]: + converted_list: List[Any] = [] + for i, item in enumerate(source): + destination_value = destination[i] + if isinstance(item, dict): + converted_list.append(deep_union_pydantic_dicts(item, destination_value)) + elif isinstance(item, list): + converted_list.append(_union_list_of_pydantic_dicts(item, destination_value)) + else: + converted_list.append(item) + return converted_list + + +def deep_union_pydantic_dicts(source: Dict[str, Any], destination: Dict[str, Any]) -> Dict[str, Any]: + for key, value in source.items(): + node = destination.setdefault(key, {}) + if isinstance(value, dict): + deep_union_pydantic_dicts(value, node) + # Note: we do not do this same processing for sets given we do not have sets of models + # and given the sets are unordered, the processing of the set and matching objects would + # be non-trivial. + elif isinstance(value, list): + destination[key] = _union_list_of_pydantic_dicts(value, node) + else: + destination[key] = value + + return destination + + +if IS_PYDANTIC_V2: + + class V2RootModel(UniversalBaseModel, pydantic.RootModel): # type: ignore[misc, name-defined, type-arg] + pass + + UniversalRootModel: TypeAlias = V2RootModel # type: ignore[misc] +else: + UniversalRootModel: TypeAlias = UniversalBaseModel # type: ignore[misc, no-redef] + + +def encode_by_type(o: Any) -> Any: + encoders_by_class_tuples: Dict[Callable[[Any], Any], Tuple[Any, ...]] = defaultdict(tuple) + for type_, encoder in encoders_by_type.items(): + encoders_by_class_tuples[encoder] += (type_,) + + if type(o) in encoders_by_type: + return encoders_by_type[type(o)](o) + for encoder, classes_tuple in encoders_by_class_tuples.items(): + if isinstance(o, classes_tuple): + return encoder(o) + + +def update_forward_refs(model: Type["Model"], **localns: Any) -> None: + if IS_PYDANTIC_V2: + model.model_rebuild(raise_errors=False) # type: ignore[attr-defined] + else: + model.update_forward_refs(**localns) + + +# Mirrors Pydantic's internal typing +AnyCallable = Callable[..., Any] + + +def universal_root_validator( + pre: bool = False, +) -> Callable[[AnyCallable], AnyCallable]: + def decorator(func: AnyCallable) -> AnyCallable: + if IS_PYDANTIC_V2: + return cast(AnyCallable, pydantic.model_validator(mode="before" if pre else "after")(func)) # type: ignore[attr-defined] + return cast(AnyCallable, pydantic.root_validator(pre=pre)(func)) # type: ignore[call-overload] + + return decorator + + +def universal_field_validator(field_name: str, pre: bool = False) -> Callable[[AnyCallable], AnyCallable]: + def decorator(func: AnyCallable) -> AnyCallable: + if IS_PYDANTIC_V2: + return cast(AnyCallable, pydantic.field_validator(field_name, mode="before" if pre else "after")(func)) # type: ignore[attr-defined] + return cast(AnyCallable, pydantic.validator(field_name, pre=pre)(func)) + + return decorator + + +PydanticField = Union[ModelField, pydantic.fields.FieldInfo] + + +def _get_model_fields(model: Type["Model"]) -> Mapping[str, PydanticField]: + if IS_PYDANTIC_V2: + return cast(Mapping[str, PydanticField], model.model_fields) # type: ignore[attr-defined] + return cast(Mapping[str, PydanticField], model.__fields__) + + +def _get_field_default(field: PydanticField) -> Any: + try: + value = field.get_default() # type: ignore[union-attr] + except: + value = field.default + if IS_PYDANTIC_V2: + from pydantic_core import PydanticUndefined + + if value == PydanticUndefined: + return None + return value + return value diff --git a/src/browser_use/core/query_encoder.py b/src/browser_use/core/query_encoder.py new file mode 100644 index 0000000..3183001 --- /dev/null +++ b/src/browser_use/core/query_encoder.py @@ -0,0 +1,58 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import Any, Dict, List, Optional, Tuple + +import pydantic + + +# Flattens dicts to be of the form {"key[subkey][subkey2]": value} where value is not a dict +def traverse_query_dict(dict_flat: Dict[str, Any], key_prefix: Optional[str] = None) -> List[Tuple[str, Any]]: + result = [] + for k, v in dict_flat.items(): + key = f"{key_prefix}[{k}]" if key_prefix is not None else k + if isinstance(v, dict): + result.extend(traverse_query_dict(v, key)) + elif isinstance(v, list): + for arr_v in v: + if isinstance(arr_v, dict): + result.extend(traverse_query_dict(arr_v, key)) + else: + result.append((key, arr_v)) + else: + result.append((key, v)) + return result + + +def single_query_encoder(query_key: str, query_value: Any) -> List[Tuple[str, Any]]: + if isinstance(query_value, pydantic.BaseModel) or isinstance(query_value, dict): + if isinstance(query_value, pydantic.BaseModel): + obj_dict = query_value.dict(by_alias=True) + else: + obj_dict = query_value + return traverse_query_dict(obj_dict, query_key) + elif isinstance(query_value, list): + encoded_values: List[Tuple[str, Any]] = [] + for value in query_value: + if isinstance(value, pydantic.BaseModel) or isinstance(value, dict): + if isinstance(value, pydantic.BaseModel): + obj_dict = value.dict(by_alias=True) + elif isinstance(value, dict): + obj_dict = value + + encoded_values.extend(single_query_encoder(query_key, obj_dict)) + else: + encoded_values.append((query_key, value)) + + return encoded_values + + return [(query_key, query_value)] + + +def encode_query(query: Optional[Dict[str, Any]]) -> Optional[List[Tuple[str, Any]]]: + if query is None: + return None + + encoded_query = [] + for k, v in query.items(): + encoded_query.extend(single_query_encoder(k, v)) + return encoded_query diff --git a/src/browser_use/core/remove_none_from_dict.py b/src/browser_use/core/remove_none_from_dict.py new file mode 100644 index 0000000..c229814 --- /dev/null +++ b/src/browser_use/core/remove_none_from_dict.py @@ -0,0 +1,11 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import Any, Dict, Mapping, Optional + + +def remove_none_from_dict(original: Mapping[str, Optional[Any]]) -> Dict[str, Any]: + new: Dict[str, Any] = {} + for key, value in original.items(): + if value is not None: + new[key] = value + return new diff --git a/src/browser_use/core/request_options.py b/src/browser_use/core/request_options.py new file mode 100644 index 0000000..1b38804 --- /dev/null +++ b/src/browser_use/core/request_options.py @@ -0,0 +1,35 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +try: + from typing import NotRequired # type: ignore +except ImportError: + from typing_extensions import NotRequired + + +class RequestOptions(typing.TypedDict, total=False): + """ + Additional options for request-specific configuration when calling APIs via the SDK. + This is used primarily as an optional final parameter for service functions. + + Attributes: + - timeout_in_seconds: int. The number of seconds to await an API call before timing out. + + - max_retries: int. The max number of retries to attempt if the API call fails. + + - additional_headers: typing.Dict[str, typing.Any]. A dictionary containing additional parameters to spread into the request's header dict + + - additional_query_parameters: typing.Dict[str, typing.Any]. A dictionary containing additional parameters to spread into the request's query parameters dict + + - additional_body_parameters: typing.Dict[str, typing.Any]. A dictionary containing additional parameters to spread into the request's body parameters dict + + - chunk_size: int. The size, in bytes, to process each chunk of data being streamed back within the response. This equates to leveraging `chunk_size` within `requests` or `httpx`, and is only leveraged for file downloads. + """ + + timeout_in_seconds: NotRequired[int] + max_retries: NotRequired[int] + additional_headers: NotRequired[typing.Dict[str, typing.Any]] + additional_query_parameters: NotRequired[typing.Dict[str, typing.Any]] + additional_body_parameters: NotRequired[typing.Dict[str, typing.Any]] + chunk_size: NotRequired[int] diff --git a/src/browser_use/core/serialization.py b/src/browser_use/core/serialization.py new file mode 100644 index 0000000..c36e865 --- /dev/null +++ b/src/browser_use/core/serialization.py @@ -0,0 +1,276 @@ +# This file was auto-generated by Fern from our API Definition. + +import collections +import inspect +import typing + +import pydantic +import typing_extensions + + +class FieldMetadata: + """ + Metadata class used to annotate fields to provide additional information. + + Example: + class MyDict(TypedDict): + field: typing.Annotated[str, FieldMetadata(alias="field_name")] + + Will serialize: `{"field": "value"}` + To: `{"field_name": "value"}` + """ + + alias: str + + def __init__(self, *, alias: str) -> None: + self.alias = alias + + +def convert_and_respect_annotation_metadata( + *, + object_: typing.Any, + annotation: typing.Any, + inner_type: typing.Optional[typing.Any] = None, + direction: typing.Literal["read", "write"], +) -> typing.Any: + """ + Respect the metadata annotations on a field, such as aliasing. This function effectively + manipulates the dict-form of an object to respect the metadata annotations. This is primarily used for + TypedDicts, which cannot support aliasing out of the box, and can be extended for additional + utilities, such as defaults. + + Parameters + ---------- + object_ : typing.Any + + annotation : type + The type we're looking to apply typing annotations from + + inner_type : typing.Optional[type] + + Returns + ------- + typing.Any + """ + + if object_ is None: + return None + if inner_type is None: + inner_type = annotation + + clean_type = _remove_annotations(inner_type) + # Pydantic models + if ( + inspect.isclass(clean_type) + and issubclass(clean_type, pydantic.BaseModel) + and isinstance(object_, typing.Mapping) + ): + return _convert_mapping(object_, clean_type, direction) + # TypedDicts + if typing_extensions.is_typeddict(clean_type) and isinstance(object_, typing.Mapping): + return _convert_mapping(object_, clean_type, direction) + + if ( + typing_extensions.get_origin(clean_type) == typing.Dict + or typing_extensions.get_origin(clean_type) == dict + or clean_type == typing.Dict + ) and isinstance(object_, typing.Dict): + key_type = typing_extensions.get_args(clean_type)[0] + value_type = typing_extensions.get_args(clean_type)[1] + + return { + key: convert_and_respect_annotation_metadata( + object_=value, + annotation=annotation, + inner_type=value_type, + direction=direction, + ) + for key, value in object_.items() + } + + # If you're iterating on a string, do not bother to coerce it to a sequence. + if not isinstance(object_, str): + if ( + typing_extensions.get_origin(clean_type) == typing.Set + or typing_extensions.get_origin(clean_type) == set + or clean_type == typing.Set + ) and isinstance(object_, typing.Set): + inner_type = typing_extensions.get_args(clean_type)[0] + return { + convert_and_respect_annotation_metadata( + object_=item, + annotation=annotation, + inner_type=inner_type, + direction=direction, + ) + for item in object_ + } + elif ( + ( + typing_extensions.get_origin(clean_type) == typing.List + or typing_extensions.get_origin(clean_type) == list + or clean_type == typing.List + ) + and isinstance(object_, typing.List) + ) or ( + ( + typing_extensions.get_origin(clean_type) == typing.Sequence + or typing_extensions.get_origin(clean_type) == collections.abc.Sequence + or clean_type == typing.Sequence + ) + and isinstance(object_, typing.Sequence) + ): + inner_type = typing_extensions.get_args(clean_type)[0] + return [ + convert_and_respect_annotation_metadata( + object_=item, + annotation=annotation, + inner_type=inner_type, + direction=direction, + ) + for item in object_ + ] + + if typing_extensions.get_origin(clean_type) == typing.Union: + # We should be able to ~relatively~ safely try to convert keys against all + # member types in the union, the edge case here is if one member aliases a field + # of the same name to a different name from another member + # Or if another member aliases a field of the same name that another member does not. + for member in typing_extensions.get_args(clean_type): + object_ = convert_and_respect_annotation_metadata( + object_=object_, + annotation=annotation, + inner_type=member, + direction=direction, + ) + return object_ + + annotated_type = _get_annotation(annotation) + if annotated_type is None: + return object_ + + # If the object is not a TypedDict, a Union, or other container (list, set, sequence, etc.) + # Then we can safely call it on the recursive conversion. + return object_ + + +def _convert_mapping( + object_: typing.Mapping[str, object], + expected_type: typing.Any, + direction: typing.Literal["read", "write"], +) -> typing.Mapping[str, object]: + converted_object: typing.Dict[str, object] = {} + try: + annotations = typing_extensions.get_type_hints(expected_type, include_extras=True) + except NameError: + # The TypedDict contains a circular reference, so + # we use the __annotations__ attribute directly. + annotations = getattr(expected_type, "__annotations__", {}) + aliases_to_field_names = _get_alias_to_field_name(annotations) + for key, value in object_.items(): + if direction == "read" and key in aliases_to_field_names: + dealiased_key = aliases_to_field_names.get(key) + if dealiased_key is not None: + type_ = annotations.get(dealiased_key) + else: + type_ = annotations.get(key) + # Note you can't get the annotation by the field name if you're in read mode, so you must check the aliases map + # + # So this is effectively saying if we're in write mode, and we don't have a type, or if we're in read mode and we don't have an alias + # then we can just pass the value through as is + if type_ is None: + converted_object[key] = value + elif direction == "read" and key not in aliases_to_field_names: + converted_object[key] = convert_and_respect_annotation_metadata( + object_=value, annotation=type_, direction=direction + ) + else: + converted_object[_alias_key(key, type_, direction, aliases_to_field_names)] = ( + convert_and_respect_annotation_metadata(object_=value, annotation=type_, direction=direction) + ) + return converted_object + + +def _get_annotation(type_: typing.Any) -> typing.Optional[typing.Any]: + maybe_annotated_type = typing_extensions.get_origin(type_) + if maybe_annotated_type is None: + return None + + if maybe_annotated_type == typing_extensions.NotRequired: + type_ = typing_extensions.get_args(type_)[0] + maybe_annotated_type = typing_extensions.get_origin(type_) + + if maybe_annotated_type == typing_extensions.Annotated: + return type_ + + return None + + +def _remove_annotations(type_: typing.Any) -> typing.Any: + maybe_annotated_type = typing_extensions.get_origin(type_) + if maybe_annotated_type is None: + return type_ + + if maybe_annotated_type == typing_extensions.NotRequired: + return _remove_annotations(typing_extensions.get_args(type_)[0]) + + if maybe_annotated_type == typing_extensions.Annotated: + return _remove_annotations(typing_extensions.get_args(type_)[0]) + + return type_ + + +def get_alias_to_field_mapping(type_: typing.Any) -> typing.Dict[str, str]: + annotations = typing_extensions.get_type_hints(type_, include_extras=True) + return _get_alias_to_field_name(annotations) + + +def get_field_to_alias_mapping(type_: typing.Any) -> typing.Dict[str, str]: + annotations = typing_extensions.get_type_hints(type_, include_extras=True) + return _get_field_to_alias_name(annotations) + + +def _get_alias_to_field_name( + field_to_hint: typing.Dict[str, typing.Any], +) -> typing.Dict[str, str]: + aliases = {} + for field, hint in field_to_hint.items(): + maybe_alias = _get_alias_from_type(hint) + if maybe_alias is not None: + aliases[maybe_alias] = field + return aliases + + +def _get_field_to_alias_name( + field_to_hint: typing.Dict[str, typing.Any], +) -> typing.Dict[str, str]: + aliases = {} + for field, hint in field_to_hint.items(): + maybe_alias = _get_alias_from_type(hint) + if maybe_alias is not None: + aliases[field] = maybe_alias + return aliases + + +def _get_alias_from_type(type_: typing.Any) -> typing.Optional[str]: + maybe_annotated_type = _get_annotation(type_) + + if maybe_annotated_type is not None: + # The actual annotations are 1 onward, the first is the annotated type + annotations = typing_extensions.get_args(maybe_annotated_type)[1:] + + for annotation in annotations: + if isinstance(annotation, FieldMetadata) and annotation.alias is not None: + return annotation.alias + return None + + +def _alias_key( + key: str, + type_: typing.Any, + direction: typing.Literal["read", "write"], + aliases_to_field_names: typing.Dict[str, str], +) -> str: + if direction == "read": + return aliases_to_field_names.get(key, key) + return _get_alias_from_type(type_=type_) or key diff --git a/src/browser_use/core/unchecked_base_model.py b/src/browser_use/core/unchecked_base_model.py new file mode 100644 index 0000000..e04a6f8 --- /dev/null +++ b/src/browser_use/core/unchecked_base_model.py @@ -0,0 +1,341 @@ +# This file was auto-generated by Fern from our API Definition. + +import datetime as dt +import inspect +import typing +import uuid + +import pydantic +import typing_extensions +from .pydantic_utilities import ( + IS_PYDANTIC_V2, + ModelField, + UniversalBaseModel, + get_args, + get_origin, + is_literal_type, + is_union, + parse_date, + parse_datetime, + parse_obj_as, +) +from .serialization import get_field_to_alias_mapping +from pydantic_core import PydanticUndefined + + +class UnionMetadata: + discriminant: str + + def __init__(self, *, discriminant: str) -> None: + self.discriminant = discriminant + + +Model = typing.TypeVar("Model", bound=pydantic.BaseModel) + + +class UncheckedBaseModel(UniversalBaseModel): + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + extra = pydantic.Extra.allow + + @classmethod + def model_construct( + cls: typing.Type["Model"], + _fields_set: typing.Optional[typing.Set[str]] = None, + **values: typing.Any, + ) -> "Model": + # Fallback construct function to the specified override below. + return cls.construct(_fields_set=_fields_set, **values) + + # Allow construct to not validate model + # Implementation taken from: https://github.com/pydantic/pydantic/issues/1168#issuecomment-817742836 + @classmethod + def construct( + cls: typing.Type["Model"], + _fields_set: typing.Optional[typing.Set[str]] = None, + **values: typing.Any, + ) -> "Model": + m = cls.__new__(cls) + fields_values = {} + + if _fields_set is None: + _fields_set = set(values.keys()) + + fields = _get_model_fields(cls) + populate_by_name = _get_is_populate_by_name(cls) + field_aliases = get_field_to_alias_mapping(cls) + + for name, field in fields.items(): + # Key here is only used to pull data from the values dict + # you should always use the NAME of the field to for field_values, etc. + # because that's how the object is constructed from a pydantic perspective + key = field.alias + if (key is None or field.alias == name) and name in field_aliases: + key = field_aliases[name] + + if key is None or (key not in values and populate_by_name): # Added this to allow population by field name + key = name + + if key in values: + if IS_PYDANTIC_V2: + type_ = field.annotation # type: ignore # Pydantic v2 + else: + type_ = typing.cast(typing.Type, field.outer_type_) # type: ignore # Pydantic < v1.10.15 + + fields_values[name] = ( + construct_type(object_=values[key], type_=type_) if type_ is not None else values[key] + ) + _fields_set.add(name) + else: + default = _get_field_default(field) + fields_values[name] = default + + # If the default values are non-null act like they've been set + # This effectively allows exclude_unset to work like exclude_none where + # the latter passes through intentionally set none values. + if default != None and default != PydanticUndefined: + _fields_set.add(name) + + # Add extras back in + extras = {} + pydantic_alias_fields = [field.alias for field in fields.values()] + internal_alias_fields = list(field_aliases.values()) + for key, value in values.items(): + # If the key is not a field by name, nor an alias to a field, then it's extra + if (key not in pydantic_alias_fields and key not in internal_alias_fields) and key not in fields: + if IS_PYDANTIC_V2: + extras[key] = value + else: + _fields_set.add(key) + fields_values[key] = value + + object.__setattr__(m, "__dict__", fields_values) + + if IS_PYDANTIC_V2: + object.__setattr__(m, "__pydantic_private__", None) + object.__setattr__(m, "__pydantic_extra__", extras) + object.__setattr__(m, "__pydantic_fields_set__", _fields_set) + else: + object.__setattr__(m, "__fields_set__", _fields_set) + m._init_private_attributes() # type: ignore # Pydantic v1 + return m + + +def _validate_collection_items_compatible(collection: typing.Any, target_type: typing.Type[typing.Any]) -> bool: + """ + Validate that all items in a collection are compatible with the target type. + + Args: + collection: The collection to validate (list, set, or dict values) + target_type: The target type to validate against + + Returns: + True if all items are compatible, False otherwise + """ + if inspect.isclass(target_type) and issubclass(target_type, pydantic.BaseModel): + for item in collection: + try: + # Try to validate the item against the target type + if isinstance(item, dict): + parse_obj_as(target_type, item) + else: + # If it's not a dict, it might already be the right type + if not isinstance(item, target_type): + return False + except Exception: + return False + return True + + +def _convert_undiscriminated_union_type(union_type: typing.Type[typing.Any], object_: typing.Any) -> typing.Any: + inner_types = get_args(union_type) + if typing.Any in inner_types: + return object_ + + for inner_type in inner_types: + # Handle lists of objects that need parsing + if get_origin(inner_type) is list and isinstance(object_, list): + list_inner_type = get_args(inner_type)[0] + try: + if inspect.isclass(list_inner_type) and issubclass(list_inner_type, pydantic.BaseModel): + # Validate that all items in the list are compatible with the target type + if _validate_collection_items_compatible(object_, list_inner_type): + parsed_list = [parse_obj_as(object_=item, type_=list_inner_type) for item in object_] + return parsed_list + except Exception: + pass + + try: + if inspect.isclass(inner_type) and issubclass(inner_type, pydantic.BaseModel): + # Attempt a validated parse until one works + return parse_obj_as(inner_type, object_) + except Exception: + continue + + # If none of the types work, just return the first successful cast + for inner_type in inner_types: + try: + return construct_type(object_=object_, type_=inner_type) + except Exception: + continue + + +def _convert_union_type(type_: typing.Type[typing.Any], object_: typing.Any) -> typing.Any: + base_type = get_origin(type_) or type_ + union_type = type_ + if base_type == typing_extensions.Annotated: + union_type = get_args(type_)[0] + annotated_metadata = get_args(type_)[1:] + for metadata in annotated_metadata: + if isinstance(metadata, UnionMetadata): + try: + # Cast to the correct type, based on the discriminant + for inner_type in get_args(union_type): + try: + objects_discriminant = getattr(object_, metadata.discriminant) + except: + objects_discriminant = object_[metadata.discriminant] + if inner_type.__fields__[metadata.discriminant].default == objects_discriminant: + return construct_type(object_=object_, type_=inner_type) + except Exception: + # Allow to fall through to our regular union handling + pass + return _convert_undiscriminated_union_type(union_type, object_) + + +def construct_type(*, type_: typing.Type[typing.Any], object_: typing.Any) -> typing.Any: + """ + Here we are essentially creating the same `construct` method in spirit as the above, but for all types, not just + Pydantic models. + The idea is to essentially attempt to coerce object_ to type_ (recursively) + """ + # Short circuit when dealing with optionals, don't try to coerces None to a type + if object_ is None: + return None + + base_type = get_origin(type_) or type_ + is_annotated = base_type == typing_extensions.Annotated + maybe_annotation_members = get_args(type_) + is_annotated_union = is_annotated and is_union(get_origin(maybe_annotation_members[0])) + + if base_type == typing.Any: + return object_ + + if base_type == dict: + if not isinstance(object_, typing.Mapping): + return object_ + + key_type, items_type = get_args(type_) + d = { + construct_type(object_=key, type_=key_type): construct_type(object_=item, type_=items_type) + for key, item in object_.items() + } + return d + + if base_type == list: + if not isinstance(object_, list): + return object_ + + inner_type = get_args(type_)[0] + return [construct_type(object_=entry, type_=inner_type) for entry in object_] + + if base_type == set: + if not isinstance(object_, set) and not isinstance(object_, list): + return object_ + + inner_type = get_args(type_)[0] + return {construct_type(object_=entry, type_=inner_type) for entry in object_} + + if is_union(base_type) or is_annotated_union: + return _convert_union_type(type_, object_) + + # Cannot do an `issubclass` with a literal type, let's also just confirm we have a class before this call + if ( + object_ is not None + and not is_literal_type(type_) + and ( + (inspect.isclass(base_type) and issubclass(base_type, pydantic.BaseModel)) + or ( + is_annotated + and inspect.isclass(maybe_annotation_members[0]) + and issubclass(maybe_annotation_members[0], pydantic.BaseModel) + ) + ) + ): + if IS_PYDANTIC_V2: + return type_.model_construct(**object_) + else: + return type_.construct(**object_) + + if base_type == dt.datetime: + try: + return parse_datetime(object_) + except Exception: + return object_ + + if base_type == dt.date: + try: + return parse_date(object_) + except Exception: + return object_ + + if base_type == uuid.UUID: + try: + return uuid.UUID(object_) + except Exception: + return object_ + + if base_type == int: + try: + return int(object_) + except Exception: + return object_ + + if base_type == bool: + try: + if isinstance(object_, str): + stringified_object = object_.lower() + return stringified_object == "true" or stringified_object == "1" + + return bool(object_) + except Exception: + return object_ + + return object_ + + +def _get_is_populate_by_name(model: typing.Type["Model"]) -> bool: + if IS_PYDANTIC_V2: + return model.model_config.get("populate_by_name", False) # type: ignore # Pydantic v2 + return model.__config__.allow_population_by_field_name # type: ignore # Pydantic v1 + + +PydanticField = typing.Union[ModelField, pydantic.fields.FieldInfo] + + +# Pydantic V1 swapped the typing of __fields__'s values from ModelField to FieldInfo +# And so we try to handle both V1 cases, as well as V2 (FieldInfo from model.model_fields) +def _get_model_fields( + model: typing.Type["Model"], +) -> typing.Mapping[str, PydanticField]: + if IS_PYDANTIC_V2: + return model.model_fields # type: ignore # Pydantic v2 + else: + return model.__fields__ # type: ignore # Pydantic v1 + + +def _get_field_default(field: PydanticField) -> typing.Any: + try: + value = field.get_default() # type: ignore # Pydantic < v1.10.15 + except: + value = field.default + if IS_PYDANTIC_V2: + from pydantic_core import PydanticUndefined + + if value == PydanticUndefined: + return None + return value + return value diff --git a/src/browser_use/environment.py b/src/browser_use/environment.py new file mode 100644 index 0000000..3d6dc8a --- /dev/null +++ b/src/browser_use/environment.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import enum + + +class BrowserUseEnvironment(enum.Enum): + PRODUCTION = "https://api.browser-use.com/api/v2" diff --git a/src/browser_use/errors/__init__.py b/src/browser_use/errors/__init__.py new file mode 100644 index 0000000..5cde020 --- /dev/null +++ b/src/browser_use/errors/__init__.py @@ -0,0 +1,4 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + diff --git a/src/browser_use/errors/bad_request_error.py b/src/browser_use/errors/bad_request_error.py new file mode 100644 index 0000000..baf5be4 --- /dev/null +++ b/src/browser_use/errors/bad_request_error.py @@ -0,0 +1,10 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ..core.api_error import ApiError + + +class BadRequestError(ApiError): + def __init__(self, body: typing.Optional[typing.Any], headers: typing.Optional[typing.Dict[str, str]] = None): + super().__init__(status_code=400, headers=headers, body=body) diff --git a/src/browser_use/errors/internal_server_error.py b/src/browser_use/errors/internal_server_error.py new file mode 100644 index 0000000..14313ab --- /dev/null +++ b/src/browser_use/errors/internal_server_error.py @@ -0,0 +1,10 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ..core.api_error import ApiError + + +class InternalServerError(ApiError): + def __init__(self, body: typing.Optional[typing.Any], headers: typing.Optional[typing.Dict[str, str]] = None): + super().__init__(status_code=500, headers=headers, body=body) diff --git a/src/browser_use/errors/not_found_error.py b/src/browser_use/errors/not_found_error.py new file mode 100644 index 0000000..dcd60e3 --- /dev/null +++ b/src/browser_use/errors/not_found_error.py @@ -0,0 +1,10 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ..core.api_error import ApiError + + +class NotFoundError(ApiError): + def __init__(self, body: typing.Optional[typing.Any], headers: typing.Optional[typing.Dict[str, str]] = None): + super().__init__(status_code=404, headers=headers, body=body) diff --git a/src/browser_use/errors/payment_required_error.py b/src/browser_use/errors/payment_required_error.py new file mode 100644 index 0000000..414ec60 --- /dev/null +++ b/src/browser_use/errors/payment_required_error.py @@ -0,0 +1,11 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ..core.api_error import ApiError +from ..types.insufficient_credits_error import InsufficientCreditsError + + +class PaymentRequiredError(ApiError): + def __init__(self, body: InsufficientCreditsError, headers: typing.Optional[typing.Dict[str, str]] = None): + super().__init__(status_code=402, headers=headers, body=body) diff --git a/src/browser_use/errors/unprocessable_entity_error.py b/src/browser_use/errors/unprocessable_entity_error.py new file mode 100644 index 0000000..93cb1ab --- /dev/null +++ b/src/browser_use/errors/unprocessable_entity_error.py @@ -0,0 +1,10 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ..core.api_error import ApiError + + +class UnprocessableEntityError(ApiError): + def __init__(self, body: typing.Optional[typing.Any], headers: typing.Optional[typing.Dict[str, str]] = None): + super().__init__(status_code=422, headers=headers, body=body) diff --git a/src/browser_use/files/__init__.py b/src/browser_use/files/__init__.py new file mode 100644 index 0000000..5cde020 --- /dev/null +++ b/src/browser_use/files/__init__.py @@ -0,0 +1,4 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + diff --git a/src/browser_use/files/client.py b/src/browser_use/files/client.py new file mode 100644 index 0000000..8824f2b --- /dev/null +++ b/src/browser_use/files/client.py @@ -0,0 +1,245 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from ..core.request_options import RequestOptions +from ..types.task_output_file_response import TaskOutputFileResponse +from ..types.upload_file_presigned_url_response import UploadFilePresignedUrlResponse +from .raw_client import AsyncRawFilesClient, RawFilesClient +from .types.upload_file_request_content_type import UploadFileRequestContentType + +# this is used as the default value for optional parameters +OMIT = typing.cast(typing.Any, ...) + + +class FilesClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._raw_client = RawFilesClient(client_wrapper=client_wrapper) + + @property + def with_raw_response(self) -> RawFilesClient: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + RawFilesClient + """ + return self._raw_client + + def user_upload_file_presigned_url( + self, + session_id: str, + *, + file_name: str, + content_type: UploadFileRequestContentType, + size_bytes: int, + request_options: typing.Optional[RequestOptions] = None, + ) -> UploadFilePresignedUrlResponse: + """ + Generate a secure presigned URL for uploading files that AI agents can use during tasks. + + Parameters + ---------- + session_id : str + + file_name : str + The name of the file to upload + + content_type : UploadFileRequestContentType + The content type of the file to upload + + size_bytes : int + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + UploadFilePresignedUrlResponse + Successful Response + + Examples + -------- + from browser_use import BrowserUse + + client = BrowserUse( + api_key="YOUR_API_KEY", + ) + client.files.user_upload_file_presigned_url( + session_id="session_id", + file_name="fileName", + content_type="image/jpg", + size_bytes=1, + ) + """ + _response = self._raw_client.user_upload_file_presigned_url( + session_id, + file_name=file_name, + content_type=content_type, + size_bytes=size_bytes, + request_options=request_options, + ) + return _response.data + + def get_task_output_file_presigned_url( + self, task_id: str, file_id: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> TaskOutputFileResponse: + """ + Get secure download URL for an output file generated by the AI agent. + + Parameters + ---------- + task_id : str + + file_id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + TaskOutputFileResponse + Successful Response + + Examples + -------- + from browser_use import BrowserUse + + client = BrowserUse( + api_key="YOUR_API_KEY", + ) + client.files.get_task_output_file_presigned_url( + task_id="task_id", + file_id="file_id", + ) + """ + _response = self._raw_client.get_task_output_file_presigned_url( + task_id, file_id, request_options=request_options + ) + return _response.data + + +class AsyncFilesClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._raw_client = AsyncRawFilesClient(client_wrapper=client_wrapper) + + @property + def with_raw_response(self) -> AsyncRawFilesClient: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + AsyncRawFilesClient + """ + return self._raw_client + + async def user_upload_file_presigned_url( + self, + session_id: str, + *, + file_name: str, + content_type: UploadFileRequestContentType, + size_bytes: int, + request_options: typing.Optional[RequestOptions] = None, + ) -> UploadFilePresignedUrlResponse: + """ + Generate a secure presigned URL for uploading files that AI agents can use during tasks. + + Parameters + ---------- + session_id : str + + file_name : str + The name of the file to upload + + content_type : UploadFileRequestContentType + The content type of the file to upload + + size_bytes : int + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + UploadFilePresignedUrlResponse + Successful Response + + Examples + -------- + import asyncio + + from browser_use import AsyncBrowserUse + + client = AsyncBrowserUse( + api_key="YOUR_API_KEY", + ) + + + async def main() -> None: + await client.files.user_upload_file_presigned_url( + session_id="session_id", + file_name="fileName", + content_type="image/jpg", + size_bytes=1, + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.user_upload_file_presigned_url( + session_id, + file_name=file_name, + content_type=content_type, + size_bytes=size_bytes, + request_options=request_options, + ) + return _response.data + + async def get_task_output_file_presigned_url( + self, task_id: str, file_id: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> TaskOutputFileResponse: + """ + Get secure download URL for an output file generated by the AI agent. + + Parameters + ---------- + task_id : str + + file_id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + TaskOutputFileResponse + Successful Response + + Examples + -------- + import asyncio + + from browser_use import AsyncBrowserUse + + client = AsyncBrowserUse( + api_key="YOUR_API_KEY", + ) + + + async def main() -> None: + await client.files.get_task_output_file_presigned_url( + task_id="task_id", + file_id="file_id", + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.get_task_output_file_presigned_url( + task_id, file_id, request_options=request_options + ) + return _response.data diff --git a/src/browser_use/files/raw_client.py b/src/browser_use/files/raw_client.py new file mode 100644 index 0000000..c269b87 --- /dev/null +++ b/src/browser_use/files/raw_client.py @@ -0,0 +1,387 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing +from json.decoder import JSONDecodeError + +from ..core.api_error import ApiError +from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from ..core.http_response import AsyncHttpResponse, HttpResponse +from ..core.jsonable_encoder import jsonable_encoder +from ..core.request_options import RequestOptions +from ..core.unchecked_base_model import construct_type +from ..errors.bad_request_error import BadRequestError +from ..errors.internal_server_error import InternalServerError +from ..errors.not_found_error import NotFoundError +from ..errors.unprocessable_entity_error import UnprocessableEntityError +from ..types.task_output_file_response import TaskOutputFileResponse +from ..types.upload_file_presigned_url_response import UploadFilePresignedUrlResponse +from .types.upload_file_request_content_type import UploadFileRequestContentType + +# this is used as the default value for optional parameters +OMIT = typing.cast(typing.Any, ...) + + +class RawFilesClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._client_wrapper = client_wrapper + + def user_upload_file_presigned_url( + self, + session_id: str, + *, + file_name: str, + content_type: UploadFileRequestContentType, + size_bytes: int, + request_options: typing.Optional[RequestOptions] = None, + ) -> HttpResponse[UploadFilePresignedUrlResponse]: + """ + Generate a secure presigned URL for uploading files that AI agents can use during tasks. + + Parameters + ---------- + session_id : str + + file_name : str + The name of the file to upload + + content_type : UploadFileRequestContentType + The content type of the file to upload + + size_bytes : int + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[UploadFilePresignedUrlResponse] + Successful Response + """ + _response = self._client_wrapper.httpx_client.request( + f"files/sessions/{jsonable_encoder(session_id)}/presigned-url", + method="POST", + json={ + "fileName": file_name, + "contentType": content_type, + "sizeBytes": size_bytes, + }, + headers={ + "content-type": "application/json", + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + UploadFilePresignedUrlResponse, + construct_type( + type_=UploadFilePresignedUrlResponse, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + typing.Optional[typing.Any], + construct_type( + type_=typing.Optional[typing.Any], # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Optional[typing.Any], + construct_type( + type_=typing.Optional[typing.Any], # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 422: + raise UnprocessableEntityError( + headers=dict(_response.headers), + body=typing.cast( + typing.Optional[typing.Any], + construct_type( + type_=typing.Optional[typing.Any], # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 500: + raise InternalServerError( + headers=dict(_response.headers), + body=typing.cast( + typing.Optional[typing.Any], + construct_type( + type_=typing.Optional[typing.Any], # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + def get_task_output_file_presigned_url( + self, task_id: str, file_id: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> HttpResponse[TaskOutputFileResponse]: + """ + Get secure download URL for an output file generated by the AI agent. + + Parameters + ---------- + task_id : str + + file_id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[TaskOutputFileResponse] + Successful Response + """ + _response = self._client_wrapper.httpx_client.request( + f"files/tasks/{jsonable_encoder(task_id)}/output-files/{jsonable_encoder(file_id)}", + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + TaskOutputFileResponse, + construct_type( + type_=TaskOutputFileResponse, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Optional[typing.Any], + construct_type( + type_=typing.Optional[typing.Any], # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 422: + raise UnprocessableEntityError( + headers=dict(_response.headers), + body=typing.cast( + typing.Optional[typing.Any], + construct_type( + type_=typing.Optional[typing.Any], # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 500: + raise InternalServerError( + headers=dict(_response.headers), + body=typing.cast( + typing.Optional[typing.Any], + construct_type( + type_=typing.Optional[typing.Any], # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + +class AsyncRawFilesClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._client_wrapper = client_wrapper + + async def user_upload_file_presigned_url( + self, + session_id: str, + *, + file_name: str, + content_type: UploadFileRequestContentType, + size_bytes: int, + request_options: typing.Optional[RequestOptions] = None, + ) -> AsyncHttpResponse[UploadFilePresignedUrlResponse]: + """ + Generate a secure presigned URL for uploading files that AI agents can use during tasks. + + Parameters + ---------- + session_id : str + + file_name : str + The name of the file to upload + + content_type : UploadFileRequestContentType + The content type of the file to upload + + size_bytes : int + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[UploadFilePresignedUrlResponse] + Successful Response + """ + _response = await self._client_wrapper.httpx_client.request( + f"files/sessions/{jsonable_encoder(session_id)}/presigned-url", + method="POST", + json={ + "fileName": file_name, + "contentType": content_type, + "sizeBytes": size_bytes, + }, + headers={ + "content-type": "application/json", + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + UploadFilePresignedUrlResponse, + construct_type( + type_=UploadFilePresignedUrlResponse, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + typing.Optional[typing.Any], + construct_type( + type_=typing.Optional[typing.Any], # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Optional[typing.Any], + construct_type( + type_=typing.Optional[typing.Any], # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 422: + raise UnprocessableEntityError( + headers=dict(_response.headers), + body=typing.cast( + typing.Optional[typing.Any], + construct_type( + type_=typing.Optional[typing.Any], # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 500: + raise InternalServerError( + headers=dict(_response.headers), + body=typing.cast( + typing.Optional[typing.Any], + construct_type( + type_=typing.Optional[typing.Any], # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + async def get_task_output_file_presigned_url( + self, task_id: str, file_id: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> AsyncHttpResponse[TaskOutputFileResponse]: + """ + Get secure download URL for an output file generated by the AI agent. + + Parameters + ---------- + task_id : str + + file_id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[TaskOutputFileResponse] + Successful Response + """ + _response = await self._client_wrapper.httpx_client.request( + f"files/tasks/{jsonable_encoder(task_id)}/output-files/{jsonable_encoder(file_id)}", + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + TaskOutputFileResponse, + construct_type( + type_=TaskOutputFileResponse, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Optional[typing.Any], + construct_type( + type_=typing.Optional[typing.Any], # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 422: + raise UnprocessableEntityError( + headers=dict(_response.headers), + body=typing.cast( + typing.Optional[typing.Any], + construct_type( + type_=typing.Optional[typing.Any], # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 500: + raise InternalServerError( + headers=dict(_response.headers), + body=typing.cast( + typing.Optional[typing.Any], + construct_type( + type_=typing.Optional[typing.Any], # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) diff --git a/src/browser_use/files/types/__init__.py b/src/browser_use/files/types/__init__.py new file mode 100644 index 0000000..5cde020 --- /dev/null +++ b/src/browser_use/files/types/__init__.py @@ -0,0 +1,4 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + diff --git a/src/browser_use/files/types/upload_file_request_content_type.py b/src/browser_use/files/types/upload_file_request_content_type.py new file mode 100644 index 0000000..9930434 --- /dev/null +++ b/src/browser_use/files/types/upload_file_request_content_type.py @@ -0,0 +1,23 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +UploadFileRequestContentType = typing.Union[ + typing.Literal[ + "image/jpg", + "image/jpeg", + "image/png", + "image/gif", + "image/webp", + "image/svg+xml", + "application/pdf", + "application/msword", + "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + "application/vnd.ms-excel", + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + "text/plain", + "text/csv", + "text/markdown", + ], + typing.Any, +] diff --git a/src/browser_use_sdk/lib/webhooks.py b/src/browser_use/lib/webhooks.py similarity index 98% rename from src/browser_use_sdk/lib/webhooks.py rename to src/browser_use/lib/webhooks.py index 1a8f1d2..bb9e110 100644 --- a/src/browser_use_sdk/lib/webhooks.py +++ b/src/browser_use/lib/webhooks.py @@ -1,8 +1,8 @@ +import hashlib import hmac import json -import hashlib -from typing import Any, Dict, Union, Literal, Optional from datetime import datetime +from typing import Any, Dict, Literal, Optional, Union from pydantic import BaseModel diff --git a/src/browser_use/profiles/__init__.py b/src/browser_use/profiles/__init__.py new file mode 100644 index 0000000..5cde020 --- /dev/null +++ b/src/browser_use/profiles/__init__.py @@ -0,0 +1,4 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + diff --git a/src/browser_use/profiles/client.py b/src/browser_use/profiles/client.py new file mode 100644 index 0000000..5d5c658 --- /dev/null +++ b/src/browser_use/profiles/client.py @@ -0,0 +1,335 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from ..core.request_options import RequestOptions +from ..types.profile_list_response import ProfileListResponse +from ..types.profile_view import ProfileView +from .raw_client import AsyncRawProfilesClient, RawProfilesClient + + +class ProfilesClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._raw_client = RawProfilesClient(client_wrapper=client_wrapper) + + @property + def with_raw_response(self) -> RawProfilesClient: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + RawProfilesClient + """ + return self._raw_client + + def list_profiles( + self, + *, + page_size: typing.Optional[int] = None, + page_number: typing.Optional[int] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> ProfileListResponse: + """ + Get paginated list of profiles. + + Parameters + ---------- + page_size : typing.Optional[int] + + page_number : typing.Optional[int] + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + ProfileListResponse + Successful Response + + Examples + -------- + from browser_use import BrowserUse + + client = BrowserUse( + api_key="YOUR_API_KEY", + ) + client.profiles.list_profiles() + """ + _response = self._raw_client.list_profiles( + page_size=page_size, page_number=page_number, request_options=request_options + ) + return _response.data + + def create_profile(self, *, request_options: typing.Optional[RequestOptions] = None) -> ProfileView: + """ + Profiles allow you to preserve the state of the browser between tasks. + + They are most commonly used to allow users to preserve the log-in state in the agent between tasks. + You'd normally create one profile per user and then use it for all their tasks. + + You can create a new profile by calling this endpoint. + + Parameters + ---------- + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + ProfileView + Successful Response + + Examples + -------- + from browser_use import BrowserUse + + client = BrowserUse( + api_key="YOUR_API_KEY", + ) + client.profiles.create_profile() + """ + _response = self._raw_client.create_profile(request_options=request_options) + return _response.data + + def get_profile(self, profile_id: str, *, request_options: typing.Optional[RequestOptions] = None) -> ProfileView: + """ + Get profile details. + + Parameters + ---------- + profile_id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + ProfileView + Successful Response + + Examples + -------- + from browser_use import BrowserUse + + client = BrowserUse( + api_key="YOUR_API_KEY", + ) + client.profiles.get_profile( + profile_id="profile_id", + ) + """ + _response = self._raw_client.get_profile(profile_id, request_options=request_options) + return _response.data + + def delete_browser_profile( + self, profile_id: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> None: + """ + Permanently delete a browser profile and its configuration. + + Parameters + ---------- + profile_id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + None + + Examples + -------- + from browser_use import BrowserUse + + client = BrowserUse( + api_key="YOUR_API_KEY", + ) + client.profiles.delete_browser_profile( + profile_id="profile_id", + ) + """ + _response = self._raw_client.delete_browser_profile(profile_id, request_options=request_options) + return _response.data + + +class AsyncProfilesClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._raw_client = AsyncRawProfilesClient(client_wrapper=client_wrapper) + + @property + def with_raw_response(self) -> AsyncRawProfilesClient: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + AsyncRawProfilesClient + """ + return self._raw_client + + async def list_profiles( + self, + *, + page_size: typing.Optional[int] = None, + page_number: typing.Optional[int] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> ProfileListResponse: + """ + Get paginated list of profiles. + + Parameters + ---------- + page_size : typing.Optional[int] + + page_number : typing.Optional[int] + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + ProfileListResponse + Successful Response + + Examples + -------- + import asyncio + + from browser_use import AsyncBrowserUse + + client = AsyncBrowserUse( + api_key="YOUR_API_KEY", + ) + + + async def main() -> None: + await client.profiles.list_profiles() + + + asyncio.run(main()) + """ + _response = await self._raw_client.list_profiles( + page_size=page_size, page_number=page_number, request_options=request_options + ) + return _response.data + + async def create_profile(self, *, request_options: typing.Optional[RequestOptions] = None) -> ProfileView: + """ + Profiles allow you to preserve the state of the browser between tasks. + + They are most commonly used to allow users to preserve the log-in state in the agent between tasks. + You'd normally create one profile per user and then use it for all their tasks. + + You can create a new profile by calling this endpoint. + + Parameters + ---------- + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + ProfileView + Successful Response + + Examples + -------- + import asyncio + + from browser_use import AsyncBrowserUse + + client = AsyncBrowserUse( + api_key="YOUR_API_KEY", + ) + + + async def main() -> None: + await client.profiles.create_profile() + + + asyncio.run(main()) + """ + _response = await self._raw_client.create_profile(request_options=request_options) + return _response.data + + async def get_profile( + self, profile_id: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> ProfileView: + """ + Get profile details. + + Parameters + ---------- + profile_id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + ProfileView + Successful Response + + Examples + -------- + import asyncio + + from browser_use import AsyncBrowserUse + + client = AsyncBrowserUse( + api_key="YOUR_API_KEY", + ) + + + async def main() -> None: + await client.profiles.get_profile( + profile_id="profile_id", + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.get_profile(profile_id, request_options=request_options) + return _response.data + + async def delete_browser_profile( + self, profile_id: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> None: + """ + Permanently delete a browser profile and its configuration. + + Parameters + ---------- + profile_id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + None + + Examples + -------- + import asyncio + + from browser_use import AsyncBrowserUse + + client = AsyncBrowserUse( + api_key="YOUR_API_KEY", + ) + + + async def main() -> None: + await client.profiles.delete_browser_profile( + profile_id="profile_id", + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.delete_browser_profile(profile_id, request_options=request_options) + return _response.data diff --git a/src/browser_use/profiles/raw_client.py b/src/browser_use/profiles/raw_client.py new file mode 100644 index 0000000..c5694b5 --- /dev/null +++ b/src/browser_use/profiles/raw_client.py @@ -0,0 +1,471 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing +from json.decoder import JSONDecodeError + +from ..core.api_error import ApiError +from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from ..core.http_response import AsyncHttpResponse, HttpResponse +from ..core.jsonable_encoder import jsonable_encoder +from ..core.request_options import RequestOptions +from ..core.unchecked_base_model import construct_type +from ..errors.not_found_error import NotFoundError +from ..errors.payment_required_error import PaymentRequiredError +from ..errors.unprocessable_entity_error import UnprocessableEntityError +from ..types.insufficient_credits_error import InsufficientCreditsError +from ..types.profile_list_response import ProfileListResponse +from ..types.profile_view import ProfileView + + +class RawProfilesClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._client_wrapper = client_wrapper + + def list_profiles( + self, + *, + page_size: typing.Optional[int] = None, + page_number: typing.Optional[int] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> HttpResponse[ProfileListResponse]: + """ + Get paginated list of profiles. + + Parameters + ---------- + page_size : typing.Optional[int] + + page_number : typing.Optional[int] + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[ProfileListResponse] + Successful Response + """ + _response = self._client_wrapper.httpx_client.request( + "profiles", + method="GET", + params={ + "pageSize": page_size, + "pageNumber": page_number, + }, + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + ProfileListResponse, + construct_type( + type_=ProfileListResponse, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 422: + raise UnprocessableEntityError( + headers=dict(_response.headers), + body=typing.cast( + typing.Optional[typing.Any], + construct_type( + type_=typing.Optional[typing.Any], # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + def create_profile(self, *, request_options: typing.Optional[RequestOptions] = None) -> HttpResponse[ProfileView]: + """ + Profiles allow you to preserve the state of the browser between tasks. + + They are most commonly used to allow users to preserve the log-in state in the agent between tasks. + You'd normally create one profile per user and then use it for all their tasks. + + You can create a new profile by calling this endpoint. + + Parameters + ---------- + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[ProfileView] + Successful Response + """ + _response = self._client_wrapper.httpx_client.request( + "profiles", + method="POST", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + ProfileView, + construct_type( + type_=ProfileView, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 402: + raise PaymentRequiredError( + headers=dict(_response.headers), + body=typing.cast( + InsufficientCreditsError, + construct_type( + type_=InsufficientCreditsError, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 422: + raise UnprocessableEntityError( + headers=dict(_response.headers), + body=typing.cast( + typing.Optional[typing.Any], + construct_type( + type_=typing.Optional[typing.Any], # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + def get_profile( + self, profile_id: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> HttpResponse[ProfileView]: + """ + Get profile details. + + Parameters + ---------- + profile_id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[ProfileView] + Successful Response + """ + _response = self._client_wrapper.httpx_client.request( + f"profiles/{jsonable_encoder(profile_id)}", + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + ProfileView, + construct_type( + type_=ProfileView, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Optional[typing.Any], + construct_type( + type_=typing.Optional[typing.Any], # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 422: + raise UnprocessableEntityError( + headers=dict(_response.headers), + body=typing.cast( + typing.Optional[typing.Any], + construct_type( + type_=typing.Optional[typing.Any], # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + def delete_browser_profile( + self, profile_id: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> HttpResponse[None]: + """ + Permanently delete a browser profile and its configuration. + + Parameters + ---------- + profile_id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[None] + """ + _response = self._client_wrapper.httpx_client.request( + f"profiles/{jsonable_encoder(profile_id)}", + method="DELETE", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + return HttpResponse(response=_response, data=None) + if _response.status_code == 422: + raise UnprocessableEntityError( + headers=dict(_response.headers), + body=typing.cast( + typing.Optional[typing.Any], + construct_type( + type_=typing.Optional[typing.Any], # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + +class AsyncRawProfilesClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._client_wrapper = client_wrapper + + async def list_profiles( + self, + *, + page_size: typing.Optional[int] = None, + page_number: typing.Optional[int] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> AsyncHttpResponse[ProfileListResponse]: + """ + Get paginated list of profiles. + + Parameters + ---------- + page_size : typing.Optional[int] + + page_number : typing.Optional[int] + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[ProfileListResponse] + Successful Response + """ + _response = await self._client_wrapper.httpx_client.request( + "profiles", + method="GET", + params={ + "pageSize": page_size, + "pageNumber": page_number, + }, + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + ProfileListResponse, + construct_type( + type_=ProfileListResponse, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 422: + raise UnprocessableEntityError( + headers=dict(_response.headers), + body=typing.cast( + typing.Optional[typing.Any], + construct_type( + type_=typing.Optional[typing.Any], # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + async def create_profile( + self, *, request_options: typing.Optional[RequestOptions] = None + ) -> AsyncHttpResponse[ProfileView]: + """ + Profiles allow you to preserve the state of the browser between tasks. + + They are most commonly used to allow users to preserve the log-in state in the agent between tasks. + You'd normally create one profile per user and then use it for all their tasks. + + You can create a new profile by calling this endpoint. + + Parameters + ---------- + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[ProfileView] + Successful Response + """ + _response = await self._client_wrapper.httpx_client.request( + "profiles", + method="POST", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + ProfileView, + construct_type( + type_=ProfileView, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 402: + raise PaymentRequiredError( + headers=dict(_response.headers), + body=typing.cast( + InsufficientCreditsError, + construct_type( + type_=InsufficientCreditsError, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 422: + raise UnprocessableEntityError( + headers=dict(_response.headers), + body=typing.cast( + typing.Optional[typing.Any], + construct_type( + type_=typing.Optional[typing.Any], # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + async def get_profile( + self, profile_id: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> AsyncHttpResponse[ProfileView]: + """ + Get profile details. + + Parameters + ---------- + profile_id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[ProfileView] + Successful Response + """ + _response = await self._client_wrapper.httpx_client.request( + f"profiles/{jsonable_encoder(profile_id)}", + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + ProfileView, + construct_type( + type_=ProfileView, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Optional[typing.Any], + construct_type( + type_=typing.Optional[typing.Any], # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 422: + raise UnprocessableEntityError( + headers=dict(_response.headers), + body=typing.cast( + typing.Optional[typing.Any], + construct_type( + type_=typing.Optional[typing.Any], # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + async def delete_browser_profile( + self, profile_id: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> AsyncHttpResponse[None]: + """ + Permanently delete a browser profile and its configuration. + + Parameters + ---------- + profile_id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[None] + """ + _response = await self._client_wrapper.httpx_client.request( + f"profiles/{jsonable_encoder(profile_id)}", + method="DELETE", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + return AsyncHttpResponse(response=_response, data=None) + if _response.status_code == 422: + raise UnprocessableEntityError( + headers=dict(_response.headers), + body=typing.cast( + typing.Optional[typing.Any], + construct_type( + type_=typing.Optional[typing.Any], # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) diff --git a/src/browser_use_sdk/py.typed b/src/browser_use/py.typed similarity index 100% rename from src/browser_use_sdk/py.typed rename to src/browser_use/py.typed diff --git a/src/browser_use/sessions/__init__.py b/src/browser_use/sessions/__init__.py new file mode 100644 index 0000000..5cde020 --- /dev/null +++ b/src/browser_use/sessions/__init__.py @@ -0,0 +1,4 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + diff --git a/src/browser_use/sessions/client.py b/src/browser_use/sessions/client.py new file mode 100644 index 0000000..606cde5 --- /dev/null +++ b/src/browser_use/sessions/client.py @@ -0,0 +1,648 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from ..core.request_options import RequestOptions +from ..types.proxy_country_code import ProxyCountryCode +from ..types.session_item_view import SessionItemView +from ..types.session_list_response import SessionListResponse +from ..types.session_status import SessionStatus +from ..types.session_view import SessionView +from ..types.share_view import ShareView +from .raw_client import AsyncRawSessionsClient, RawSessionsClient + +# this is used as the default value for optional parameters +OMIT = typing.cast(typing.Any, ...) + + +class SessionsClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._raw_client = RawSessionsClient(client_wrapper=client_wrapper) + + @property + def with_raw_response(self) -> RawSessionsClient: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + RawSessionsClient + """ + return self._raw_client + + def list_sessions( + self, + *, + page_size: typing.Optional[int] = None, + page_number: typing.Optional[int] = None, + filter_by: typing.Optional[SessionStatus] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> SessionListResponse: + """ + Get paginated list of AI agent sessions with optional status filtering. + + Parameters + ---------- + page_size : typing.Optional[int] + + page_number : typing.Optional[int] + + filter_by : typing.Optional[SessionStatus] + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + SessionListResponse + Successful Response + + Examples + -------- + from browser_use import BrowserUse + + client = BrowserUse( + api_key="YOUR_API_KEY", + ) + client.sessions.list_sessions() + """ + _response = self._raw_client.list_sessions( + page_size=page_size, page_number=page_number, filter_by=filter_by, request_options=request_options + ) + return _response.data + + def create_session( + self, + *, + profile_id: typing.Optional[str] = OMIT, + proxy_country_code: typing.Optional[ProxyCountryCode] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> SessionItemView: + """ + Create a new session with a new task. + + Parameters + ---------- + profile_id : typing.Optional[str] + The ID of the profile to use for the session + + proxy_country_code : typing.Optional[ProxyCountryCode] + Country code for proxy location. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + SessionItemView + Successful Response + + Examples + -------- + from browser_use import BrowserUse + + client = BrowserUse( + api_key="YOUR_API_KEY", + ) + client.sessions.create_session() + """ + _response = self._raw_client.create_session( + profile_id=profile_id, proxy_country_code=proxy_country_code, request_options=request_options + ) + return _response.data + + def get_session(self, session_id: str, *, request_options: typing.Optional[RequestOptions] = None) -> SessionView: + """ + Get detailed session information including status, URLs, and task details. + + Parameters + ---------- + session_id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + SessionView + Successful Response + + Examples + -------- + from browser_use import BrowserUse + + client = BrowserUse( + api_key="YOUR_API_KEY", + ) + client.sessions.get_session( + session_id="session_id", + ) + """ + _response = self._raw_client.get_session(session_id, request_options=request_options) + return _response.data + + def delete_session(self, session_id: str, *, request_options: typing.Optional[RequestOptions] = None) -> None: + """ + Permanently delete a session and all associated data. + + Parameters + ---------- + session_id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + None + + Examples + -------- + from browser_use import BrowserUse + + client = BrowserUse( + api_key="YOUR_API_KEY", + ) + client.sessions.delete_session( + session_id="session_id", + ) + """ + _response = self._raw_client.delete_session(session_id, request_options=request_options) + return _response.data + + def update_session( + self, session_id: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> SessionView: + """ + Stop a session and all its running tasks. + + Parameters + ---------- + session_id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + SessionView + Successful Response + + Examples + -------- + from browser_use import BrowserUse + + client = BrowserUse( + api_key="YOUR_API_KEY", + ) + client.sessions.update_session( + session_id="session_id", + ) + """ + _response = self._raw_client.update_session(session_id, request_options=request_options) + return _response.data + + def get_session_public_share( + self, session_id: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> ShareView: + """ + Get public share information including URL and usage statistics. + + Parameters + ---------- + session_id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + ShareView + Successful Response + + Examples + -------- + from browser_use import BrowserUse + + client = BrowserUse( + api_key="YOUR_API_KEY", + ) + client.sessions.get_session_public_share( + session_id="session_id", + ) + """ + _response = self._raw_client.get_session_public_share(session_id, request_options=request_options) + return _response.data + + def create_session_public_share( + self, session_id: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> ShareView: + """ + Create or return existing public share for a session. + + Parameters + ---------- + session_id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + ShareView + Successful Response + + Examples + -------- + from browser_use import BrowserUse + + client = BrowserUse( + api_key="YOUR_API_KEY", + ) + client.sessions.create_session_public_share( + session_id="session_id", + ) + """ + _response = self._raw_client.create_session_public_share(session_id, request_options=request_options) + return _response.data + + def delete_session_public_share( + self, session_id: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> None: + """ + Remove public share for a session. + + Parameters + ---------- + session_id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + None + + Examples + -------- + from browser_use import BrowserUse + + client = BrowserUse( + api_key="YOUR_API_KEY", + ) + client.sessions.delete_session_public_share( + session_id="session_id", + ) + """ + _response = self._raw_client.delete_session_public_share(session_id, request_options=request_options) + return _response.data + + +class AsyncSessionsClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._raw_client = AsyncRawSessionsClient(client_wrapper=client_wrapper) + + @property + def with_raw_response(self) -> AsyncRawSessionsClient: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + AsyncRawSessionsClient + """ + return self._raw_client + + async def list_sessions( + self, + *, + page_size: typing.Optional[int] = None, + page_number: typing.Optional[int] = None, + filter_by: typing.Optional[SessionStatus] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> SessionListResponse: + """ + Get paginated list of AI agent sessions with optional status filtering. + + Parameters + ---------- + page_size : typing.Optional[int] + + page_number : typing.Optional[int] + + filter_by : typing.Optional[SessionStatus] + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + SessionListResponse + Successful Response + + Examples + -------- + import asyncio + + from browser_use import AsyncBrowserUse + + client = AsyncBrowserUse( + api_key="YOUR_API_KEY", + ) + + + async def main() -> None: + await client.sessions.list_sessions() + + + asyncio.run(main()) + """ + _response = await self._raw_client.list_sessions( + page_size=page_size, page_number=page_number, filter_by=filter_by, request_options=request_options + ) + return _response.data + + async def create_session( + self, + *, + profile_id: typing.Optional[str] = OMIT, + proxy_country_code: typing.Optional[ProxyCountryCode] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> SessionItemView: + """ + Create a new session with a new task. + + Parameters + ---------- + profile_id : typing.Optional[str] + The ID of the profile to use for the session + + proxy_country_code : typing.Optional[ProxyCountryCode] + Country code for proxy location. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + SessionItemView + Successful Response + + Examples + -------- + import asyncio + + from browser_use import AsyncBrowserUse + + client = AsyncBrowserUse( + api_key="YOUR_API_KEY", + ) + + + async def main() -> None: + await client.sessions.create_session() + + + asyncio.run(main()) + """ + _response = await self._raw_client.create_session( + profile_id=profile_id, proxy_country_code=proxy_country_code, request_options=request_options + ) + return _response.data + + async def get_session( + self, session_id: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> SessionView: + """ + Get detailed session information including status, URLs, and task details. + + Parameters + ---------- + session_id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + SessionView + Successful Response + + Examples + -------- + import asyncio + + from browser_use import AsyncBrowserUse + + client = AsyncBrowserUse( + api_key="YOUR_API_KEY", + ) + + + async def main() -> None: + await client.sessions.get_session( + session_id="session_id", + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.get_session(session_id, request_options=request_options) + return _response.data + + async def delete_session(self, session_id: str, *, request_options: typing.Optional[RequestOptions] = None) -> None: + """ + Permanently delete a session and all associated data. + + Parameters + ---------- + session_id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + None + + Examples + -------- + import asyncio + + from browser_use import AsyncBrowserUse + + client = AsyncBrowserUse( + api_key="YOUR_API_KEY", + ) + + + async def main() -> None: + await client.sessions.delete_session( + session_id="session_id", + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.delete_session(session_id, request_options=request_options) + return _response.data + + async def update_session( + self, session_id: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> SessionView: + """ + Stop a session and all its running tasks. + + Parameters + ---------- + session_id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + SessionView + Successful Response + + Examples + -------- + import asyncio + + from browser_use import AsyncBrowserUse + + client = AsyncBrowserUse( + api_key="YOUR_API_KEY", + ) + + + async def main() -> None: + await client.sessions.update_session( + session_id="session_id", + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.update_session(session_id, request_options=request_options) + return _response.data + + async def get_session_public_share( + self, session_id: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> ShareView: + """ + Get public share information including URL and usage statistics. + + Parameters + ---------- + session_id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + ShareView + Successful Response + + Examples + -------- + import asyncio + + from browser_use import AsyncBrowserUse + + client = AsyncBrowserUse( + api_key="YOUR_API_KEY", + ) + + + async def main() -> None: + await client.sessions.get_session_public_share( + session_id="session_id", + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.get_session_public_share(session_id, request_options=request_options) + return _response.data + + async def create_session_public_share( + self, session_id: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> ShareView: + """ + Create or return existing public share for a session. + + Parameters + ---------- + session_id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + ShareView + Successful Response + + Examples + -------- + import asyncio + + from browser_use import AsyncBrowserUse + + client = AsyncBrowserUse( + api_key="YOUR_API_KEY", + ) + + + async def main() -> None: + await client.sessions.create_session_public_share( + session_id="session_id", + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.create_session_public_share(session_id, request_options=request_options) + return _response.data + + async def delete_session_public_share( + self, session_id: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> None: + """ + Remove public share for a session. + + Parameters + ---------- + session_id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + None + + Examples + -------- + import asyncio + + from browser_use import AsyncBrowserUse + + client = AsyncBrowserUse( + api_key="YOUR_API_KEY", + ) + + + async def main() -> None: + await client.sessions.delete_session_public_share( + session_id="session_id", + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.delete_session_public_share(session_id, request_options=request_options) + return _response.data diff --git a/src/browser_use/sessions/raw_client.py b/src/browser_use/sessions/raw_client.py new file mode 100644 index 0000000..80a6cd8 --- /dev/null +++ b/src/browser_use/sessions/raw_client.py @@ -0,0 +1,990 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing +from json.decoder import JSONDecodeError + +from ..core.api_error import ApiError +from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from ..core.http_response import AsyncHttpResponse, HttpResponse +from ..core.jsonable_encoder import jsonable_encoder +from ..core.request_options import RequestOptions +from ..core.unchecked_base_model import construct_type +from ..errors.not_found_error import NotFoundError +from ..errors.unprocessable_entity_error import UnprocessableEntityError +from ..types.proxy_country_code import ProxyCountryCode +from ..types.session_item_view import SessionItemView +from ..types.session_list_response import SessionListResponse +from ..types.session_status import SessionStatus +from ..types.session_view import SessionView +from ..types.share_view import ShareView + +# this is used as the default value for optional parameters +OMIT = typing.cast(typing.Any, ...) + + +class RawSessionsClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._client_wrapper = client_wrapper + + def list_sessions( + self, + *, + page_size: typing.Optional[int] = None, + page_number: typing.Optional[int] = None, + filter_by: typing.Optional[SessionStatus] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> HttpResponse[SessionListResponse]: + """ + Get paginated list of AI agent sessions with optional status filtering. + + Parameters + ---------- + page_size : typing.Optional[int] + + page_number : typing.Optional[int] + + filter_by : typing.Optional[SessionStatus] + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[SessionListResponse] + Successful Response + """ + _response = self._client_wrapper.httpx_client.request( + "sessions", + method="GET", + params={ + "pageSize": page_size, + "pageNumber": page_number, + "filterBy": filter_by, + }, + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + SessionListResponse, + construct_type( + type_=SessionListResponse, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 422: + raise UnprocessableEntityError( + headers=dict(_response.headers), + body=typing.cast( + typing.Optional[typing.Any], + construct_type( + type_=typing.Optional[typing.Any], # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + def create_session( + self, + *, + profile_id: typing.Optional[str] = OMIT, + proxy_country_code: typing.Optional[ProxyCountryCode] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> HttpResponse[SessionItemView]: + """ + Create a new session with a new task. + + Parameters + ---------- + profile_id : typing.Optional[str] + The ID of the profile to use for the session + + proxy_country_code : typing.Optional[ProxyCountryCode] + Country code for proxy location. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[SessionItemView] + Successful Response + """ + _response = self._client_wrapper.httpx_client.request( + "sessions", + method="POST", + json={ + "profileId": profile_id, + "proxyCountryCode": proxy_country_code, + }, + headers={ + "content-type": "application/json", + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + SessionItemView, + construct_type( + type_=SessionItemView, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Optional[typing.Any], + construct_type( + type_=typing.Optional[typing.Any], # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 422: + raise UnprocessableEntityError( + headers=dict(_response.headers), + body=typing.cast( + typing.Optional[typing.Any], + construct_type( + type_=typing.Optional[typing.Any], # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + def get_session( + self, session_id: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> HttpResponse[SessionView]: + """ + Get detailed session information including status, URLs, and task details. + + Parameters + ---------- + session_id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[SessionView] + Successful Response + """ + _response = self._client_wrapper.httpx_client.request( + f"sessions/{jsonable_encoder(session_id)}", + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + SessionView, + construct_type( + type_=SessionView, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Optional[typing.Any], + construct_type( + type_=typing.Optional[typing.Any], # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 422: + raise UnprocessableEntityError( + headers=dict(_response.headers), + body=typing.cast( + typing.Optional[typing.Any], + construct_type( + type_=typing.Optional[typing.Any], # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + def delete_session( + self, session_id: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> HttpResponse[None]: + """ + Permanently delete a session and all associated data. + + Parameters + ---------- + session_id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[None] + """ + _response = self._client_wrapper.httpx_client.request( + f"sessions/{jsonable_encoder(session_id)}", + method="DELETE", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + return HttpResponse(response=_response, data=None) + if _response.status_code == 422: + raise UnprocessableEntityError( + headers=dict(_response.headers), + body=typing.cast( + typing.Optional[typing.Any], + construct_type( + type_=typing.Optional[typing.Any], # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + def update_session( + self, session_id: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> HttpResponse[SessionView]: + """ + Stop a session and all its running tasks. + + Parameters + ---------- + session_id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[SessionView] + Successful Response + """ + _response = self._client_wrapper.httpx_client.request( + f"sessions/{jsonable_encoder(session_id)}", + method="PATCH", + json={ + "action": "stop", + }, + headers={ + "content-type": "application/json", + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + SessionView, + construct_type( + type_=SessionView, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Optional[typing.Any], + construct_type( + type_=typing.Optional[typing.Any], # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 422: + raise UnprocessableEntityError( + headers=dict(_response.headers), + body=typing.cast( + typing.Optional[typing.Any], + construct_type( + type_=typing.Optional[typing.Any], # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + def get_session_public_share( + self, session_id: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> HttpResponse[ShareView]: + """ + Get public share information including URL and usage statistics. + + Parameters + ---------- + session_id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[ShareView] + Successful Response + """ + _response = self._client_wrapper.httpx_client.request( + f"sessions/{jsonable_encoder(session_id)}/public-share", + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + ShareView, + construct_type( + type_=ShareView, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Optional[typing.Any], + construct_type( + type_=typing.Optional[typing.Any], # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 422: + raise UnprocessableEntityError( + headers=dict(_response.headers), + body=typing.cast( + typing.Optional[typing.Any], + construct_type( + type_=typing.Optional[typing.Any], # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + def create_session_public_share( + self, session_id: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> HttpResponse[ShareView]: + """ + Create or return existing public share for a session. + + Parameters + ---------- + session_id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[ShareView] + Successful Response + """ + _response = self._client_wrapper.httpx_client.request( + f"sessions/{jsonable_encoder(session_id)}/public-share", + method="POST", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + ShareView, + construct_type( + type_=ShareView, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Optional[typing.Any], + construct_type( + type_=typing.Optional[typing.Any], # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 422: + raise UnprocessableEntityError( + headers=dict(_response.headers), + body=typing.cast( + typing.Optional[typing.Any], + construct_type( + type_=typing.Optional[typing.Any], # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + def delete_session_public_share( + self, session_id: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> HttpResponse[None]: + """ + Remove public share for a session. + + Parameters + ---------- + session_id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[None] + """ + _response = self._client_wrapper.httpx_client.request( + f"sessions/{jsonable_encoder(session_id)}/public-share", + method="DELETE", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + return HttpResponse(response=_response, data=None) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Optional[typing.Any], + construct_type( + type_=typing.Optional[typing.Any], # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 422: + raise UnprocessableEntityError( + headers=dict(_response.headers), + body=typing.cast( + typing.Optional[typing.Any], + construct_type( + type_=typing.Optional[typing.Any], # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + +class AsyncRawSessionsClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._client_wrapper = client_wrapper + + async def list_sessions( + self, + *, + page_size: typing.Optional[int] = None, + page_number: typing.Optional[int] = None, + filter_by: typing.Optional[SessionStatus] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> AsyncHttpResponse[SessionListResponse]: + """ + Get paginated list of AI agent sessions with optional status filtering. + + Parameters + ---------- + page_size : typing.Optional[int] + + page_number : typing.Optional[int] + + filter_by : typing.Optional[SessionStatus] + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[SessionListResponse] + Successful Response + """ + _response = await self._client_wrapper.httpx_client.request( + "sessions", + method="GET", + params={ + "pageSize": page_size, + "pageNumber": page_number, + "filterBy": filter_by, + }, + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + SessionListResponse, + construct_type( + type_=SessionListResponse, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 422: + raise UnprocessableEntityError( + headers=dict(_response.headers), + body=typing.cast( + typing.Optional[typing.Any], + construct_type( + type_=typing.Optional[typing.Any], # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + async def create_session( + self, + *, + profile_id: typing.Optional[str] = OMIT, + proxy_country_code: typing.Optional[ProxyCountryCode] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> AsyncHttpResponse[SessionItemView]: + """ + Create a new session with a new task. + + Parameters + ---------- + profile_id : typing.Optional[str] + The ID of the profile to use for the session + + proxy_country_code : typing.Optional[ProxyCountryCode] + Country code for proxy location. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[SessionItemView] + Successful Response + """ + _response = await self._client_wrapper.httpx_client.request( + "sessions", + method="POST", + json={ + "profileId": profile_id, + "proxyCountryCode": proxy_country_code, + }, + headers={ + "content-type": "application/json", + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + SessionItemView, + construct_type( + type_=SessionItemView, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Optional[typing.Any], + construct_type( + type_=typing.Optional[typing.Any], # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 422: + raise UnprocessableEntityError( + headers=dict(_response.headers), + body=typing.cast( + typing.Optional[typing.Any], + construct_type( + type_=typing.Optional[typing.Any], # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + async def get_session( + self, session_id: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> AsyncHttpResponse[SessionView]: + """ + Get detailed session information including status, URLs, and task details. + + Parameters + ---------- + session_id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[SessionView] + Successful Response + """ + _response = await self._client_wrapper.httpx_client.request( + f"sessions/{jsonable_encoder(session_id)}", + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + SessionView, + construct_type( + type_=SessionView, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Optional[typing.Any], + construct_type( + type_=typing.Optional[typing.Any], # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 422: + raise UnprocessableEntityError( + headers=dict(_response.headers), + body=typing.cast( + typing.Optional[typing.Any], + construct_type( + type_=typing.Optional[typing.Any], # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + async def delete_session( + self, session_id: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> AsyncHttpResponse[None]: + """ + Permanently delete a session and all associated data. + + Parameters + ---------- + session_id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[None] + """ + _response = await self._client_wrapper.httpx_client.request( + f"sessions/{jsonable_encoder(session_id)}", + method="DELETE", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + return AsyncHttpResponse(response=_response, data=None) + if _response.status_code == 422: + raise UnprocessableEntityError( + headers=dict(_response.headers), + body=typing.cast( + typing.Optional[typing.Any], + construct_type( + type_=typing.Optional[typing.Any], # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + async def update_session( + self, session_id: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> AsyncHttpResponse[SessionView]: + """ + Stop a session and all its running tasks. + + Parameters + ---------- + session_id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[SessionView] + Successful Response + """ + _response = await self._client_wrapper.httpx_client.request( + f"sessions/{jsonable_encoder(session_id)}", + method="PATCH", + json={ + "action": "stop", + }, + headers={ + "content-type": "application/json", + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + SessionView, + construct_type( + type_=SessionView, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Optional[typing.Any], + construct_type( + type_=typing.Optional[typing.Any], # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 422: + raise UnprocessableEntityError( + headers=dict(_response.headers), + body=typing.cast( + typing.Optional[typing.Any], + construct_type( + type_=typing.Optional[typing.Any], # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + async def get_session_public_share( + self, session_id: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> AsyncHttpResponse[ShareView]: + """ + Get public share information including URL and usage statistics. + + Parameters + ---------- + session_id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[ShareView] + Successful Response + """ + _response = await self._client_wrapper.httpx_client.request( + f"sessions/{jsonable_encoder(session_id)}/public-share", + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + ShareView, + construct_type( + type_=ShareView, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Optional[typing.Any], + construct_type( + type_=typing.Optional[typing.Any], # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 422: + raise UnprocessableEntityError( + headers=dict(_response.headers), + body=typing.cast( + typing.Optional[typing.Any], + construct_type( + type_=typing.Optional[typing.Any], # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + async def create_session_public_share( + self, session_id: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> AsyncHttpResponse[ShareView]: + """ + Create or return existing public share for a session. + + Parameters + ---------- + session_id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[ShareView] + Successful Response + """ + _response = await self._client_wrapper.httpx_client.request( + f"sessions/{jsonable_encoder(session_id)}/public-share", + method="POST", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + ShareView, + construct_type( + type_=ShareView, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Optional[typing.Any], + construct_type( + type_=typing.Optional[typing.Any], # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 422: + raise UnprocessableEntityError( + headers=dict(_response.headers), + body=typing.cast( + typing.Optional[typing.Any], + construct_type( + type_=typing.Optional[typing.Any], # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + async def delete_session_public_share( + self, session_id: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> AsyncHttpResponse[None]: + """ + Remove public share for a session. + + Parameters + ---------- + session_id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[None] + """ + _response = await self._client_wrapper.httpx_client.request( + f"sessions/{jsonable_encoder(session_id)}/public-share", + method="DELETE", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + return AsyncHttpResponse(response=_response, data=None) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Optional[typing.Any], + construct_type( + type_=typing.Optional[typing.Any], # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 422: + raise UnprocessableEntityError( + headers=dict(_response.headers), + body=typing.cast( + typing.Optional[typing.Any], + construct_type( + type_=typing.Optional[typing.Any], # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) diff --git a/src/browser_use/tasks/__init__.py b/src/browser_use/tasks/__init__.py new file mode 100644 index 0000000..5cde020 --- /dev/null +++ b/src/browser_use/tasks/__init__.py @@ -0,0 +1,4 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + diff --git a/src/browser_use/tasks/client.py b/src/browser_use/tasks/client.py new file mode 100644 index 0000000..5d2574e --- /dev/null +++ b/src/browser_use/tasks/client.py @@ -0,0 +1,610 @@ +# This file was auto-generated by Fern from our API Definition. + +import datetime as dt +import typing + +from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from ..core.request_options import RequestOptions +from ..types.supported_ll_ms import SupportedLlMs +from ..types.task_created_response import TaskCreatedResponse +from ..types.task_list_response import TaskListResponse +from ..types.task_log_file_response import TaskLogFileResponse +from ..types.task_status import TaskStatus +from ..types.task_update_action import TaskUpdateAction +from ..types.task_view import TaskView +from .raw_client import AsyncRawTasksClient, RawTasksClient + +# this is used as the default value for optional parameters +OMIT = typing.cast(typing.Any, ...) + + +class TasksClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._raw_client = RawTasksClient(client_wrapper=client_wrapper) + + @property + def with_raw_response(self) -> RawTasksClient: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + RawTasksClient + """ + return self._raw_client + + def list_tasks( + self, + *, + page_size: typing.Optional[int] = None, + page_number: typing.Optional[int] = None, + session_id: typing.Optional[str] = None, + filter_by: typing.Optional[TaskStatus] = None, + after: typing.Optional[dt.datetime] = None, + before: typing.Optional[dt.datetime] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> TaskListResponse: + """ + Get paginated list of AI agent tasks with optional filtering by session and status. + + Parameters + ---------- + page_size : typing.Optional[int] + + page_number : typing.Optional[int] + + session_id : typing.Optional[str] + + filter_by : typing.Optional[TaskStatus] + + after : typing.Optional[dt.datetime] + + before : typing.Optional[dt.datetime] + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + TaskListResponse + Successful Response + + Examples + -------- + from browser_use import BrowserUse + + client = BrowserUse( + api_key="YOUR_API_KEY", + ) + client.tasks.list_tasks() + """ + _response = self._raw_client.list_tasks( + page_size=page_size, + page_number=page_number, + session_id=session_id, + filter_by=filter_by, + after=after, + before=before, + request_options=request_options, + ) + return _response.data + + def create_task( + self, + *, + task: str, + llm: typing.Optional[SupportedLlMs] = OMIT, + start_url: typing.Optional[str] = OMIT, + max_steps: typing.Optional[int] = OMIT, + structured_output: typing.Optional[str] = OMIT, + session_id: typing.Optional[str] = OMIT, + metadata: typing.Optional[typing.Dict[str, typing.Optional[str]]] = OMIT, + secrets: typing.Optional[typing.Dict[str, typing.Optional[str]]] = OMIT, + allowed_domains: typing.Optional[typing.Sequence[str]] = OMIT, + highlight_elements: typing.Optional[bool] = OMIT, + flash_mode: typing.Optional[bool] = OMIT, + thinking: typing.Optional[bool] = OMIT, + vision: typing.Optional[bool] = OMIT, + system_prompt_extension: typing.Optional[str] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> TaskCreatedResponse: + """ + You can either: + 1. Start a new task (auto creates a new simple session) + 2. Start a new task in an existing session (you can create a custom session before starting the task and reuse it for follow-up tasks) + + Parameters + ---------- + task : str + The task prompt/instruction for the agent. + + llm : typing.Optional[SupportedLlMs] + The LLM model to use for the agent. + + start_url : typing.Optional[str] + The URL to start the task from. + + max_steps : typing.Optional[int] + Maximum number of steps the agent can take before stopping. + + structured_output : typing.Optional[str] + The stringified JSON schema for the structured output. + + session_id : typing.Optional[str] + The ID of the session where the task will run. + + metadata : typing.Optional[typing.Dict[str, typing.Optional[str]]] + The metadata for the task. + + secrets : typing.Optional[typing.Dict[str, typing.Optional[str]]] + The secrets for the task. + + allowed_domains : typing.Optional[typing.Sequence[str]] + The allowed domains for the task. + + highlight_elements : typing.Optional[bool] + Tells the agent to highlight interactive elements on the page. + + flash_mode : typing.Optional[bool] + Whether agent flash mode is enabled. + + thinking : typing.Optional[bool] + Whether agent thinking mode is enabled. + + vision : typing.Optional[bool] + Whether agent vision capabilities are enabled. + + system_prompt_extension : typing.Optional[str] + Optional extension to the agent system prompt. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + TaskCreatedResponse + Successful Response + + Examples + -------- + from browser_use import BrowserUse + + client = BrowserUse( + api_key="YOUR_API_KEY", + ) + client.tasks.create_task( + task="task", + ) + """ + _response = self._raw_client.create_task( + task=task, + llm=llm, + start_url=start_url, + max_steps=max_steps, + structured_output=structured_output, + session_id=session_id, + metadata=metadata, + secrets=secrets, + allowed_domains=allowed_domains, + highlight_elements=highlight_elements, + flash_mode=flash_mode, + thinking=thinking, + vision=vision, + system_prompt_extension=system_prompt_extension, + request_options=request_options, + ) + return _response.data + + def get_task(self, task_id: str, *, request_options: typing.Optional[RequestOptions] = None) -> TaskView: + """ + Get detailed task information including status, progress, steps, and file outputs. + + Parameters + ---------- + task_id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + TaskView + Successful Response + + Examples + -------- + from browser_use import BrowserUse + + client = BrowserUse( + api_key="YOUR_API_KEY", + ) + client.tasks.get_task( + task_id="task_id", + ) + """ + _response = self._raw_client.get_task(task_id, request_options=request_options) + return _response.data + + def update_task( + self, task_id: str, *, action: TaskUpdateAction, request_options: typing.Optional[RequestOptions] = None + ) -> TaskView: + """ + Control task execution with stop, pause, resume, or stop task and session actions. + + Parameters + ---------- + task_id : str + + action : TaskUpdateAction + The action to perform on the task + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + TaskView + Successful Response + + Examples + -------- + from browser_use import BrowserUse + + client = BrowserUse( + api_key="YOUR_API_KEY", + ) + client.tasks.update_task( + task_id="task_id", + action="stop", + ) + """ + _response = self._raw_client.update_task(task_id, action=action, request_options=request_options) + return _response.data + + def get_task_logs( + self, task_id: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> TaskLogFileResponse: + """ + Get secure download URL for task execution logs with step-by-step details. + + Parameters + ---------- + task_id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + TaskLogFileResponse + Successful Response + + Examples + -------- + from browser_use import BrowserUse + + client = BrowserUse( + api_key="YOUR_API_KEY", + ) + client.tasks.get_task_logs( + task_id="task_id", + ) + """ + _response = self._raw_client.get_task_logs(task_id, request_options=request_options) + return _response.data + + +class AsyncTasksClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._raw_client = AsyncRawTasksClient(client_wrapper=client_wrapper) + + @property + def with_raw_response(self) -> AsyncRawTasksClient: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + AsyncRawTasksClient + """ + return self._raw_client + + async def list_tasks( + self, + *, + page_size: typing.Optional[int] = None, + page_number: typing.Optional[int] = None, + session_id: typing.Optional[str] = None, + filter_by: typing.Optional[TaskStatus] = None, + after: typing.Optional[dt.datetime] = None, + before: typing.Optional[dt.datetime] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> TaskListResponse: + """ + Get paginated list of AI agent tasks with optional filtering by session and status. + + Parameters + ---------- + page_size : typing.Optional[int] + + page_number : typing.Optional[int] + + session_id : typing.Optional[str] + + filter_by : typing.Optional[TaskStatus] + + after : typing.Optional[dt.datetime] + + before : typing.Optional[dt.datetime] + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + TaskListResponse + Successful Response + + Examples + -------- + import asyncio + + from browser_use import AsyncBrowserUse + + client = AsyncBrowserUse( + api_key="YOUR_API_KEY", + ) + + + async def main() -> None: + await client.tasks.list_tasks() + + + asyncio.run(main()) + """ + _response = await self._raw_client.list_tasks( + page_size=page_size, + page_number=page_number, + session_id=session_id, + filter_by=filter_by, + after=after, + before=before, + request_options=request_options, + ) + return _response.data + + async def create_task( + self, + *, + task: str, + llm: typing.Optional[SupportedLlMs] = OMIT, + start_url: typing.Optional[str] = OMIT, + max_steps: typing.Optional[int] = OMIT, + structured_output: typing.Optional[str] = OMIT, + session_id: typing.Optional[str] = OMIT, + metadata: typing.Optional[typing.Dict[str, typing.Optional[str]]] = OMIT, + secrets: typing.Optional[typing.Dict[str, typing.Optional[str]]] = OMIT, + allowed_domains: typing.Optional[typing.Sequence[str]] = OMIT, + highlight_elements: typing.Optional[bool] = OMIT, + flash_mode: typing.Optional[bool] = OMIT, + thinking: typing.Optional[bool] = OMIT, + vision: typing.Optional[bool] = OMIT, + system_prompt_extension: typing.Optional[str] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> TaskCreatedResponse: + """ + You can either: + 1. Start a new task (auto creates a new simple session) + 2. Start a new task in an existing session (you can create a custom session before starting the task and reuse it for follow-up tasks) + + Parameters + ---------- + task : str + The task prompt/instruction for the agent. + + llm : typing.Optional[SupportedLlMs] + The LLM model to use for the agent. + + start_url : typing.Optional[str] + The URL to start the task from. + + max_steps : typing.Optional[int] + Maximum number of steps the agent can take before stopping. + + structured_output : typing.Optional[str] + The stringified JSON schema for the structured output. + + session_id : typing.Optional[str] + The ID of the session where the task will run. + + metadata : typing.Optional[typing.Dict[str, typing.Optional[str]]] + The metadata for the task. + + secrets : typing.Optional[typing.Dict[str, typing.Optional[str]]] + The secrets for the task. + + allowed_domains : typing.Optional[typing.Sequence[str]] + The allowed domains for the task. + + highlight_elements : typing.Optional[bool] + Tells the agent to highlight interactive elements on the page. + + flash_mode : typing.Optional[bool] + Whether agent flash mode is enabled. + + thinking : typing.Optional[bool] + Whether agent thinking mode is enabled. + + vision : typing.Optional[bool] + Whether agent vision capabilities are enabled. + + system_prompt_extension : typing.Optional[str] + Optional extension to the agent system prompt. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + TaskCreatedResponse + Successful Response + + Examples + -------- + import asyncio + + from browser_use import AsyncBrowserUse + + client = AsyncBrowserUse( + api_key="YOUR_API_KEY", + ) + + + async def main() -> None: + await client.tasks.create_task( + task="task", + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.create_task( + task=task, + llm=llm, + start_url=start_url, + max_steps=max_steps, + structured_output=structured_output, + session_id=session_id, + metadata=metadata, + secrets=secrets, + allowed_domains=allowed_domains, + highlight_elements=highlight_elements, + flash_mode=flash_mode, + thinking=thinking, + vision=vision, + system_prompt_extension=system_prompt_extension, + request_options=request_options, + ) + return _response.data + + async def get_task(self, task_id: str, *, request_options: typing.Optional[RequestOptions] = None) -> TaskView: + """ + Get detailed task information including status, progress, steps, and file outputs. + + Parameters + ---------- + task_id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + TaskView + Successful Response + + Examples + -------- + import asyncio + + from browser_use import AsyncBrowserUse + + client = AsyncBrowserUse( + api_key="YOUR_API_KEY", + ) + + + async def main() -> None: + await client.tasks.get_task( + task_id="task_id", + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.get_task(task_id, request_options=request_options) + return _response.data + + async def update_task( + self, task_id: str, *, action: TaskUpdateAction, request_options: typing.Optional[RequestOptions] = None + ) -> TaskView: + """ + Control task execution with stop, pause, resume, or stop task and session actions. + + Parameters + ---------- + task_id : str + + action : TaskUpdateAction + The action to perform on the task + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + TaskView + Successful Response + + Examples + -------- + import asyncio + + from browser_use import AsyncBrowserUse + + client = AsyncBrowserUse( + api_key="YOUR_API_KEY", + ) + + + async def main() -> None: + await client.tasks.update_task( + task_id="task_id", + action="stop", + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.update_task(task_id, action=action, request_options=request_options) + return _response.data + + async def get_task_logs( + self, task_id: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> TaskLogFileResponse: + """ + Get secure download URL for task execution logs with step-by-step details. + + Parameters + ---------- + task_id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + TaskLogFileResponse + Successful Response + + Examples + -------- + import asyncio + + from browser_use import AsyncBrowserUse + + client = AsyncBrowserUse( + api_key="YOUR_API_KEY", + ) + + + async def main() -> None: + await client.tasks.get_task_logs( + task_id="task_id", + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.get_task_logs(task_id, request_options=request_options) + return _response.data diff --git a/src/browser_use/tasks/raw_client.py b/src/browser_use/tasks/raw_client.py new file mode 100644 index 0000000..4235d34 --- /dev/null +++ b/src/browser_use/tasks/raw_client.py @@ -0,0 +1,933 @@ +# This file was auto-generated by Fern from our API Definition. + +import datetime as dt +import typing +from json.decoder import JSONDecodeError + +from ..core.api_error import ApiError +from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from ..core.datetime_utils import serialize_datetime +from ..core.http_response import AsyncHttpResponse, HttpResponse +from ..core.jsonable_encoder import jsonable_encoder +from ..core.request_options import RequestOptions +from ..core.unchecked_base_model import construct_type +from ..errors.bad_request_error import BadRequestError +from ..errors.internal_server_error import InternalServerError +from ..errors.not_found_error import NotFoundError +from ..errors.payment_required_error import PaymentRequiredError +from ..errors.unprocessable_entity_error import UnprocessableEntityError +from ..types.insufficient_credits_error import InsufficientCreditsError +from ..types.supported_ll_ms import SupportedLlMs +from ..types.task_created_response import TaskCreatedResponse +from ..types.task_list_response import TaskListResponse +from ..types.task_log_file_response import TaskLogFileResponse +from ..types.task_status import TaskStatus +from ..types.task_update_action import TaskUpdateAction +from ..types.task_view import TaskView + +# this is used as the default value for optional parameters +OMIT = typing.cast(typing.Any, ...) + + +class RawTasksClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._client_wrapper = client_wrapper + + def list_tasks( + self, + *, + page_size: typing.Optional[int] = None, + page_number: typing.Optional[int] = None, + session_id: typing.Optional[str] = None, + filter_by: typing.Optional[TaskStatus] = None, + after: typing.Optional[dt.datetime] = None, + before: typing.Optional[dt.datetime] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> HttpResponse[TaskListResponse]: + """ + Get paginated list of AI agent tasks with optional filtering by session and status. + + Parameters + ---------- + page_size : typing.Optional[int] + + page_number : typing.Optional[int] + + session_id : typing.Optional[str] + + filter_by : typing.Optional[TaskStatus] + + after : typing.Optional[dt.datetime] + + before : typing.Optional[dt.datetime] + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[TaskListResponse] + Successful Response + """ + _response = self._client_wrapper.httpx_client.request( + "tasks", + method="GET", + params={ + "pageSize": page_size, + "pageNumber": page_number, + "sessionId": session_id, + "filterBy": filter_by, + "after": serialize_datetime(after) if after is not None else None, + "before": serialize_datetime(before) if before is not None else None, + }, + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + TaskListResponse, + construct_type( + type_=TaskListResponse, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 422: + raise UnprocessableEntityError( + headers=dict(_response.headers), + body=typing.cast( + typing.Optional[typing.Any], + construct_type( + type_=typing.Optional[typing.Any], # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + def create_task( + self, + *, + task: str, + llm: typing.Optional[SupportedLlMs] = OMIT, + start_url: typing.Optional[str] = OMIT, + max_steps: typing.Optional[int] = OMIT, + structured_output: typing.Optional[str] = OMIT, + session_id: typing.Optional[str] = OMIT, + metadata: typing.Optional[typing.Dict[str, typing.Optional[str]]] = OMIT, + secrets: typing.Optional[typing.Dict[str, typing.Optional[str]]] = OMIT, + allowed_domains: typing.Optional[typing.Sequence[str]] = OMIT, + highlight_elements: typing.Optional[bool] = OMIT, + flash_mode: typing.Optional[bool] = OMIT, + thinking: typing.Optional[bool] = OMIT, + vision: typing.Optional[bool] = OMIT, + system_prompt_extension: typing.Optional[str] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> HttpResponse[TaskCreatedResponse]: + """ + You can either: + 1. Start a new task (auto creates a new simple session) + 2. Start a new task in an existing session (you can create a custom session before starting the task and reuse it for follow-up tasks) + + Parameters + ---------- + task : str + The task prompt/instruction for the agent. + + llm : typing.Optional[SupportedLlMs] + The LLM model to use for the agent. + + start_url : typing.Optional[str] + The URL to start the task from. + + max_steps : typing.Optional[int] + Maximum number of steps the agent can take before stopping. + + structured_output : typing.Optional[str] + The stringified JSON schema for the structured output. + + session_id : typing.Optional[str] + The ID of the session where the task will run. + + metadata : typing.Optional[typing.Dict[str, typing.Optional[str]]] + The metadata for the task. + + secrets : typing.Optional[typing.Dict[str, typing.Optional[str]]] + The secrets for the task. + + allowed_domains : typing.Optional[typing.Sequence[str]] + The allowed domains for the task. + + highlight_elements : typing.Optional[bool] + Tells the agent to highlight interactive elements on the page. + + flash_mode : typing.Optional[bool] + Whether agent flash mode is enabled. + + thinking : typing.Optional[bool] + Whether agent thinking mode is enabled. + + vision : typing.Optional[bool] + Whether agent vision capabilities are enabled. + + system_prompt_extension : typing.Optional[str] + Optional extension to the agent system prompt. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[TaskCreatedResponse] + Successful Response + """ + _response = self._client_wrapper.httpx_client.request( + "tasks", + method="POST", + json={ + "task": task, + "llm": llm, + "startUrl": start_url, + "maxSteps": max_steps, + "structuredOutput": structured_output, + "sessionId": session_id, + "metadata": metadata, + "secrets": secrets, + "allowedDomains": allowed_domains, + "highlightElements": highlight_elements, + "flashMode": flash_mode, + "thinking": thinking, + "vision": vision, + "systemPromptExtension": system_prompt_extension, + }, + headers={ + "content-type": "application/json", + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + TaskCreatedResponse, + construct_type( + type_=TaskCreatedResponse, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + typing.Optional[typing.Any], + construct_type( + type_=typing.Optional[typing.Any], # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 402: + raise PaymentRequiredError( + headers=dict(_response.headers), + body=typing.cast( + InsufficientCreditsError, + construct_type( + type_=InsufficientCreditsError, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Optional[typing.Any], + construct_type( + type_=typing.Optional[typing.Any], # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 422: + raise UnprocessableEntityError( + headers=dict(_response.headers), + body=typing.cast( + typing.Optional[typing.Any], + construct_type( + type_=typing.Optional[typing.Any], # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 500: + raise InternalServerError( + headers=dict(_response.headers), + body=typing.cast( + typing.Optional[typing.Any], + construct_type( + type_=typing.Optional[typing.Any], # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + def get_task( + self, task_id: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> HttpResponse[TaskView]: + """ + Get detailed task information including status, progress, steps, and file outputs. + + Parameters + ---------- + task_id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[TaskView] + Successful Response + """ + _response = self._client_wrapper.httpx_client.request( + f"tasks/{jsonable_encoder(task_id)}", + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + TaskView, + construct_type( + type_=TaskView, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Optional[typing.Any], + construct_type( + type_=typing.Optional[typing.Any], # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 422: + raise UnprocessableEntityError( + headers=dict(_response.headers), + body=typing.cast( + typing.Optional[typing.Any], + construct_type( + type_=typing.Optional[typing.Any], # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + def update_task( + self, task_id: str, *, action: TaskUpdateAction, request_options: typing.Optional[RequestOptions] = None + ) -> HttpResponse[TaskView]: + """ + Control task execution with stop, pause, resume, or stop task and session actions. + + Parameters + ---------- + task_id : str + + action : TaskUpdateAction + The action to perform on the task + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[TaskView] + Successful Response + """ + _response = self._client_wrapper.httpx_client.request( + f"tasks/{jsonable_encoder(task_id)}", + method="PATCH", + json={ + "action": action, + }, + headers={ + "content-type": "application/json", + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + TaskView, + construct_type( + type_=TaskView, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Optional[typing.Any], + construct_type( + type_=typing.Optional[typing.Any], # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 422: + raise UnprocessableEntityError( + headers=dict(_response.headers), + body=typing.cast( + typing.Optional[typing.Any], + construct_type( + type_=typing.Optional[typing.Any], # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + def get_task_logs( + self, task_id: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> HttpResponse[TaskLogFileResponse]: + """ + Get secure download URL for task execution logs with step-by-step details. + + Parameters + ---------- + task_id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[TaskLogFileResponse] + Successful Response + """ + _response = self._client_wrapper.httpx_client.request( + f"tasks/{jsonable_encoder(task_id)}/logs", + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + TaskLogFileResponse, + construct_type( + type_=TaskLogFileResponse, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Optional[typing.Any], + construct_type( + type_=typing.Optional[typing.Any], # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 422: + raise UnprocessableEntityError( + headers=dict(_response.headers), + body=typing.cast( + typing.Optional[typing.Any], + construct_type( + type_=typing.Optional[typing.Any], # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 500: + raise InternalServerError( + headers=dict(_response.headers), + body=typing.cast( + typing.Optional[typing.Any], + construct_type( + type_=typing.Optional[typing.Any], # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + +class AsyncRawTasksClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._client_wrapper = client_wrapper + + async def list_tasks( + self, + *, + page_size: typing.Optional[int] = None, + page_number: typing.Optional[int] = None, + session_id: typing.Optional[str] = None, + filter_by: typing.Optional[TaskStatus] = None, + after: typing.Optional[dt.datetime] = None, + before: typing.Optional[dt.datetime] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> AsyncHttpResponse[TaskListResponse]: + """ + Get paginated list of AI agent tasks with optional filtering by session and status. + + Parameters + ---------- + page_size : typing.Optional[int] + + page_number : typing.Optional[int] + + session_id : typing.Optional[str] + + filter_by : typing.Optional[TaskStatus] + + after : typing.Optional[dt.datetime] + + before : typing.Optional[dt.datetime] + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[TaskListResponse] + Successful Response + """ + _response = await self._client_wrapper.httpx_client.request( + "tasks", + method="GET", + params={ + "pageSize": page_size, + "pageNumber": page_number, + "sessionId": session_id, + "filterBy": filter_by, + "after": serialize_datetime(after) if after is not None else None, + "before": serialize_datetime(before) if before is not None else None, + }, + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + TaskListResponse, + construct_type( + type_=TaskListResponse, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 422: + raise UnprocessableEntityError( + headers=dict(_response.headers), + body=typing.cast( + typing.Optional[typing.Any], + construct_type( + type_=typing.Optional[typing.Any], # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + async def create_task( + self, + *, + task: str, + llm: typing.Optional[SupportedLlMs] = OMIT, + start_url: typing.Optional[str] = OMIT, + max_steps: typing.Optional[int] = OMIT, + structured_output: typing.Optional[str] = OMIT, + session_id: typing.Optional[str] = OMIT, + metadata: typing.Optional[typing.Dict[str, typing.Optional[str]]] = OMIT, + secrets: typing.Optional[typing.Dict[str, typing.Optional[str]]] = OMIT, + allowed_domains: typing.Optional[typing.Sequence[str]] = OMIT, + highlight_elements: typing.Optional[bool] = OMIT, + flash_mode: typing.Optional[bool] = OMIT, + thinking: typing.Optional[bool] = OMIT, + vision: typing.Optional[bool] = OMIT, + system_prompt_extension: typing.Optional[str] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> AsyncHttpResponse[TaskCreatedResponse]: + """ + You can either: + 1. Start a new task (auto creates a new simple session) + 2. Start a new task in an existing session (you can create a custom session before starting the task and reuse it for follow-up tasks) + + Parameters + ---------- + task : str + The task prompt/instruction for the agent. + + llm : typing.Optional[SupportedLlMs] + The LLM model to use for the agent. + + start_url : typing.Optional[str] + The URL to start the task from. + + max_steps : typing.Optional[int] + Maximum number of steps the agent can take before stopping. + + structured_output : typing.Optional[str] + The stringified JSON schema for the structured output. + + session_id : typing.Optional[str] + The ID of the session where the task will run. + + metadata : typing.Optional[typing.Dict[str, typing.Optional[str]]] + The metadata for the task. + + secrets : typing.Optional[typing.Dict[str, typing.Optional[str]]] + The secrets for the task. + + allowed_domains : typing.Optional[typing.Sequence[str]] + The allowed domains for the task. + + highlight_elements : typing.Optional[bool] + Tells the agent to highlight interactive elements on the page. + + flash_mode : typing.Optional[bool] + Whether agent flash mode is enabled. + + thinking : typing.Optional[bool] + Whether agent thinking mode is enabled. + + vision : typing.Optional[bool] + Whether agent vision capabilities are enabled. + + system_prompt_extension : typing.Optional[str] + Optional extension to the agent system prompt. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[TaskCreatedResponse] + Successful Response + """ + _response = await self._client_wrapper.httpx_client.request( + "tasks", + method="POST", + json={ + "task": task, + "llm": llm, + "startUrl": start_url, + "maxSteps": max_steps, + "structuredOutput": structured_output, + "sessionId": session_id, + "metadata": metadata, + "secrets": secrets, + "allowedDomains": allowed_domains, + "highlightElements": highlight_elements, + "flashMode": flash_mode, + "thinking": thinking, + "vision": vision, + "systemPromptExtension": system_prompt_extension, + }, + headers={ + "content-type": "application/json", + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + TaskCreatedResponse, + construct_type( + type_=TaskCreatedResponse, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + typing.Optional[typing.Any], + construct_type( + type_=typing.Optional[typing.Any], # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 402: + raise PaymentRequiredError( + headers=dict(_response.headers), + body=typing.cast( + InsufficientCreditsError, + construct_type( + type_=InsufficientCreditsError, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Optional[typing.Any], + construct_type( + type_=typing.Optional[typing.Any], # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 422: + raise UnprocessableEntityError( + headers=dict(_response.headers), + body=typing.cast( + typing.Optional[typing.Any], + construct_type( + type_=typing.Optional[typing.Any], # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 500: + raise InternalServerError( + headers=dict(_response.headers), + body=typing.cast( + typing.Optional[typing.Any], + construct_type( + type_=typing.Optional[typing.Any], # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + async def get_task( + self, task_id: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> AsyncHttpResponse[TaskView]: + """ + Get detailed task information including status, progress, steps, and file outputs. + + Parameters + ---------- + task_id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[TaskView] + Successful Response + """ + _response = await self._client_wrapper.httpx_client.request( + f"tasks/{jsonable_encoder(task_id)}", + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + TaskView, + construct_type( + type_=TaskView, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Optional[typing.Any], + construct_type( + type_=typing.Optional[typing.Any], # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 422: + raise UnprocessableEntityError( + headers=dict(_response.headers), + body=typing.cast( + typing.Optional[typing.Any], + construct_type( + type_=typing.Optional[typing.Any], # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + async def update_task( + self, task_id: str, *, action: TaskUpdateAction, request_options: typing.Optional[RequestOptions] = None + ) -> AsyncHttpResponse[TaskView]: + """ + Control task execution with stop, pause, resume, or stop task and session actions. + + Parameters + ---------- + task_id : str + + action : TaskUpdateAction + The action to perform on the task + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[TaskView] + Successful Response + """ + _response = await self._client_wrapper.httpx_client.request( + f"tasks/{jsonable_encoder(task_id)}", + method="PATCH", + json={ + "action": action, + }, + headers={ + "content-type": "application/json", + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + TaskView, + construct_type( + type_=TaskView, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Optional[typing.Any], + construct_type( + type_=typing.Optional[typing.Any], # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 422: + raise UnprocessableEntityError( + headers=dict(_response.headers), + body=typing.cast( + typing.Optional[typing.Any], + construct_type( + type_=typing.Optional[typing.Any], # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + async def get_task_logs( + self, task_id: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> AsyncHttpResponse[TaskLogFileResponse]: + """ + Get secure download URL for task execution logs with step-by-step details. + + Parameters + ---------- + task_id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[TaskLogFileResponse] + Successful Response + """ + _response = await self._client_wrapper.httpx_client.request( + f"tasks/{jsonable_encoder(task_id)}/logs", + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + TaskLogFileResponse, + construct_type( + type_=TaskLogFileResponse, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Optional[typing.Any], + construct_type( + type_=typing.Optional[typing.Any], # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 422: + raise UnprocessableEntityError( + headers=dict(_response.headers), + body=typing.cast( + typing.Optional[typing.Any], + construct_type( + type_=typing.Optional[typing.Any], # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 500: + raise InternalServerError( + headers=dict(_response.headers), + body=typing.cast( + typing.Optional[typing.Any], + construct_type( + type_=typing.Optional[typing.Any], # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) diff --git a/src/browser_use/types/__init__.py b/src/browser_use/types/__init__.py new file mode 100644 index 0000000..5cde020 --- /dev/null +++ b/src/browser_use/types/__init__.py @@ -0,0 +1,4 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + diff --git a/src/browser_use/types/account_not_found_error.py b/src/browser_use/types/account_not_found_error.py new file mode 100644 index 0000000..8b7cdae --- /dev/null +++ b/src/browser_use/types/account_not_found_error.py @@ -0,0 +1,24 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel + + +class AccountNotFoundError(UncheckedBaseModel): + """ + Error response when a account is not found + """ + + detail: typing.Optional[str] = None + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/browser_use/types/account_view.py b/src/browser_use/types/account_view.py new file mode 100644 index 0000000..0deeeb7 --- /dev/null +++ b/src/browser_use/types/account_view.py @@ -0,0 +1,54 @@ +# This file was auto-generated by Fern from our API Definition. + +import datetime as dt +import typing + +import pydantic +import typing_extensions +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.serialization import FieldMetadata +from ..core.unchecked_base_model import UncheckedBaseModel + + +class AccountView(UncheckedBaseModel): + """ + View model for account information + """ + + monthly_credits_balance_usd: typing_extensions.Annotated[float, FieldMetadata(alias="monthlyCreditsBalanceUsd")] = ( + pydantic.Field() + ) + """ + The monthly credits balance in USD + """ + + additional_credits_balance_usd: typing_extensions.Annotated[ + float, FieldMetadata(alias="additionalCreditsBalanceUsd") + ] = pydantic.Field() + """ + The additional credits balance in USD + """ + + email: typing.Optional[str] = pydantic.Field(default=None) + """ + The email address of the user + """ + + name: typing.Optional[str] = pydantic.Field(default=None) + """ + The name of the user + """ + + signed_up_at: typing_extensions.Annotated[dt.datetime, FieldMetadata(alias="signedUpAt")] = pydantic.Field() + """ + The date and time the user signed up + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/browser_use/types/bad_request_error_body.py b/src/browser_use/types/bad_request_error_body.py new file mode 100644 index 0000000..0826417 --- /dev/null +++ b/src/browser_use/types/bad_request_error_body.py @@ -0,0 +1,8 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from .session_has_running_task_error import SessionHasRunningTaskError +from .session_stopped_error import SessionStoppedError + +BadRequestErrorBody = typing.Union[SessionStoppedError, SessionHasRunningTaskError] diff --git a/src/browser_use/types/credits_deduction_error.py b/src/browser_use/types/credits_deduction_error.py new file mode 100644 index 0000000..e57637c --- /dev/null +++ b/src/browser_use/types/credits_deduction_error.py @@ -0,0 +1,24 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel + + +class CreditsDeductionError(UncheckedBaseModel): + """ + Error response when credits deduction fails + """ + + detail: typing.Optional[str] = None + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/browser_use/types/download_url_generation_error.py b/src/browser_use/types/download_url_generation_error.py new file mode 100644 index 0000000..f97eb2f --- /dev/null +++ b/src/browser_use/types/download_url_generation_error.py @@ -0,0 +1,24 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel + + +class DownloadUrlGenerationError(UncheckedBaseModel): + """ + Error response when download URL generation fails + """ + + detail: typing.Optional[str] = None + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/browser_use/types/file_view.py b/src/browser_use/types/file_view.py new file mode 100644 index 0000000..b283213 --- /dev/null +++ b/src/browser_use/types/file_view.py @@ -0,0 +1,34 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +import typing_extensions +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.serialization import FieldMetadata +from ..core.unchecked_base_model import UncheckedBaseModel + + +class FileView(UncheckedBaseModel): + """ + View model for representing an output file generated by the agent + """ + + id: str = pydantic.Field() + """ + Unique identifier for the output file + """ + + file_name: typing_extensions.Annotated[str, FieldMetadata(alias="fileName")] = pydantic.Field() + """ + Name of the output file + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/browser_use/types/http_validation_error.py b/src/browser_use/types/http_validation_error.py new file mode 100644 index 0000000..188935a --- /dev/null +++ b/src/browser_use/types/http_validation_error.py @@ -0,0 +1,21 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel +from .validation_error import ValidationError + + +class HttpValidationError(UncheckedBaseModel): + detail: typing.Optional[typing.List[ValidationError]] = None + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/browser_use/types/insufficient_credits_error.py b/src/browser_use/types/insufficient_credits_error.py new file mode 100644 index 0000000..2a141c4 --- /dev/null +++ b/src/browser_use/types/insufficient_credits_error.py @@ -0,0 +1,24 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel + + +class InsufficientCreditsError(UncheckedBaseModel): + """ + Error response when user has insufficient credits + """ + + detail: typing.Optional[str] = None + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/browser_use/types/internal_server_error_body.py b/src/browser_use/types/internal_server_error_body.py new file mode 100644 index 0000000..32825e1 --- /dev/null +++ b/src/browser_use/types/internal_server_error_body.py @@ -0,0 +1,24 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel + + +class InternalServerErrorBody(UncheckedBaseModel): + """ + Error response for internal server errors + """ + + detail: typing.Optional[str] = None + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/browser_use/types/not_found_error_body.py b/src/browser_use/types/not_found_error_body.py new file mode 100644 index 0000000..a935598 --- /dev/null +++ b/src/browser_use/types/not_found_error_body.py @@ -0,0 +1,8 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from .output_file_not_found_error import OutputFileNotFoundError +from .task_not_found_error import TaskNotFoundError + +NotFoundErrorBody = typing.Union[TaskNotFoundError, OutputFileNotFoundError] diff --git a/src/browser_use/types/output_file_not_found_error.py b/src/browser_use/types/output_file_not_found_error.py new file mode 100644 index 0000000..1f2c0dd --- /dev/null +++ b/src/browser_use/types/output_file_not_found_error.py @@ -0,0 +1,24 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel + + +class OutputFileNotFoundError(UncheckedBaseModel): + """ + Error response when an output file is not found + """ + + detail: typing.Optional[str] = None + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/browser_use/types/profile_list_response.py b/src/browser_use/types/profile_list_response.py new file mode 100644 index 0000000..dfb1a45 --- /dev/null +++ b/src/browser_use/types/profile_list_response.py @@ -0,0 +1,45 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +import typing_extensions +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.serialization import FieldMetadata +from ..core.unchecked_base_model import UncheckedBaseModel +from .profile_view import ProfileView + + +class ProfileListResponse(UncheckedBaseModel): + """ + Response model for paginated profile list requests. + """ + + items: typing.List[ProfileView] = pydantic.Field() + """ + List of profile views for the current page + """ + + total_items: typing_extensions.Annotated[int, FieldMetadata(alias="totalItems")] = pydantic.Field() + """ + Total number of items in the list + """ + + page_number: typing_extensions.Annotated[int, FieldMetadata(alias="pageNumber")] = pydantic.Field() + """ + Page number + """ + + page_size: typing_extensions.Annotated[int, FieldMetadata(alias="pageSize")] = pydantic.Field() + """ + Number of items per page + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/browser_use/types/profile_not_found_error.py b/src/browser_use/types/profile_not_found_error.py new file mode 100644 index 0000000..0c22e9c --- /dev/null +++ b/src/browser_use/types/profile_not_found_error.py @@ -0,0 +1,24 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel + + +class ProfileNotFoundError(UncheckedBaseModel): + """ + Error response when a profile is not found + """ + + detail: typing.Optional[str] = None + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/browser_use/types/profile_view.py b/src/browser_use/types/profile_view.py new file mode 100644 index 0000000..745a4a4 --- /dev/null +++ b/src/browser_use/types/profile_view.py @@ -0,0 +1,49 @@ +# This file was auto-generated by Fern from our API Definition. + +import datetime as dt +import typing + +import pydantic +import typing_extensions +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.serialization import FieldMetadata +from ..core.unchecked_base_model import UncheckedBaseModel + + +class ProfileView(UncheckedBaseModel): + """ + View model for representing a profile. A profile lets you preserve the login state between sessions. + + We recommend that you create a separate profile for each user of your app. + """ + + id: str = pydantic.Field() + """ + Unique identifier for the profile + """ + + last_used_at: typing_extensions.Annotated[typing.Optional[dt.datetime], FieldMetadata(alias="lastUsedAt")] = ( + pydantic.Field(default=None) + ) + """ + Timestamp when the profile was last used + """ + + created_at: typing_extensions.Annotated[dt.datetime, FieldMetadata(alias="createdAt")] = pydantic.Field() + """ + Timestamp when the profile was created + """ + + updated_at: typing_extensions.Annotated[dt.datetime, FieldMetadata(alias="updatedAt")] = pydantic.Field() + """ + Timestamp when the profile was last updated + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/browser_use/types/proxy_country_code.py b/src/browser_use/types/proxy_country_code.py new file mode 100644 index 0000000..e531daf --- /dev/null +++ b/src/browser_use/types/proxy_country_code.py @@ -0,0 +1,5 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +ProxyCountryCode = typing.Union[typing.Literal["us", "uk", "fr", "it", "jp", "au", "de", "fi", "ca", "in"], typing.Any] diff --git a/src/browser_use/types/session_has_running_task_error.py b/src/browser_use/types/session_has_running_task_error.py new file mode 100644 index 0000000..502761f --- /dev/null +++ b/src/browser_use/types/session_has_running_task_error.py @@ -0,0 +1,24 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel + + +class SessionHasRunningTaskError(UncheckedBaseModel): + """ + Error response when session already has a running task + """ + + detail: typing.Optional[str] = None + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/browser_use/types/session_item_view.py b/src/browser_use/types/session_item_view.py new file mode 100644 index 0000000..e14c15d --- /dev/null +++ b/src/browser_use/types/session_item_view.py @@ -0,0 +1,55 @@ +# This file was auto-generated by Fern from our API Definition. + +import datetime as dt +import typing + +import pydantic +import typing_extensions +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.serialization import FieldMetadata +from ..core.unchecked_base_model import UncheckedBaseModel +from .session_status import SessionStatus + + +class SessionItemView(UncheckedBaseModel): + """ + View model for representing a (browser) session. + """ + + id: str = pydantic.Field() + """ + Unique identifier for the session + """ + + status: SessionStatus = pydantic.Field() + """ + Current status of the session (active/stopped) + """ + + live_url: typing_extensions.Annotated[typing.Optional[str], FieldMetadata(alias="liveUrl")] = pydantic.Field( + default=None + ) + """ + URL where the browser can be viewed live in real-time + """ + + started_at: typing_extensions.Annotated[dt.datetime, FieldMetadata(alias="startedAt")] = pydantic.Field() + """ + Timestamp when the session was created and started + """ + + finished_at: typing_extensions.Annotated[typing.Optional[dt.datetime], FieldMetadata(alias="finishedAt")] = ( + pydantic.Field(default=None) + ) + """ + Timestamp when the session was stopped (None if still active) + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/browser_use/types/session_list_response.py b/src/browser_use/types/session_list_response.py new file mode 100644 index 0000000..7299478 --- /dev/null +++ b/src/browser_use/types/session_list_response.py @@ -0,0 +1,45 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +import typing_extensions +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.serialization import FieldMetadata +from ..core.unchecked_base_model import UncheckedBaseModel +from .session_item_view import SessionItemView + + +class SessionListResponse(UncheckedBaseModel): + """ + Response model for paginated session list requests. + """ + + items: typing.List[SessionItemView] = pydantic.Field() + """ + List of session views for the current page + """ + + total_items: typing_extensions.Annotated[int, FieldMetadata(alias="totalItems")] = pydantic.Field() + """ + Total number of items in the list + """ + + page_number: typing_extensions.Annotated[int, FieldMetadata(alias="pageNumber")] = pydantic.Field() + """ + Page number + """ + + page_size: typing_extensions.Annotated[int, FieldMetadata(alias="pageSize")] = pydantic.Field() + """ + Number of items per page + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/browser_use/types/session_not_found_error.py b/src/browser_use/types/session_not_found_error.py new file mode 100644 index 0000000..213aabb --- /dev/null +++ b/src/browser_use/types/session_not_found_error.py @@ -0,0 +1,24 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel + + +class SessionNotFoundError(UncheckedBaseModel): + """ + Error response when a session is not found + """ + + detail: typing.Optional[str] = None + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/browser_use/types/session_status.py b/src/browser_use/types/session_status.py new file mode 100644 index 0000000..8f037d7 --- /dev/null +++ b/src/browser_use/types/session_status.py @@ -0,0 +1,5 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +SessionStatus = typing.Union[typing.Literal["active", "stopped"], typing.Any] diff --git a/src/browser_use/types/session_stopped_error.py b/src/browser_use/types/session_stopped_error.py new file mode 100644 index 0000000..f753bf0 --- /dev/null +++ b/src/browser_use/types/session_stopped_error.py @@ -0,0 +1,24 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel + + +class SessionStoppedError(UncheckedBaseModel): + """ + Error response when trying to use a stopped session + """ + + detail: typing.Optional[str] = None + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/browser_use/types/session_update_action.py b/src/browser_use/types/session_update_action.py new file mode 100644 index 0000000..7028c98 --- /dev/null +++ b/src/browser_use/types/session_update_action.py @@ -0,0 +1,5 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +SessionUpdateAction = typing.Literal["stop"] diff --git a/src/browser_use/types/session_view.py b/src/browser_use/types/session_view.py new file mode 100644 index 0000000..f2e67e3 --- /dev/null +++ b/src/browser_use/types/session_view.py @@ -0,0 +1,68 @@ +# This file was auto-generated by Fern from our API Definition. + +import datetime as dt +import typing + +import pydantic +import typing_extensions +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.serialization import FieldMetadata +from ..core.unchecked_base_model import UncheckedBaseModel +from .session_status import SessionStatus +from .task_item_view import TaskItemView + + +class SessionView(UncheckedBaseModel): + """ + View model for representing a (browser) session with its associated tasks. + """ + + id: str = pydantic.Field() + """ + Unique identifier for the session + """ + + status: SessionStatus = pydantic.Field() + """ + Current status of the session (active/stopped) + """ + + live_url: typing_extensions.Annotated[typing.Optional[str], FieldMetadata(alias="liveUrl")] = pydantic.Field( + default=None + ) + """ + URL where the browser can be viewed live in real-time + """ + + started_at: typing_extensions.Annotated[dt.datetime, FieldMetadata(alias="startedAt")] = pydantic.Field() + """ + Timestamp when the session was created and started + """ + + finished_at: typing_extensions.Annotated[typing.Optional[dt.datetime], FieldMetadata(alias="finishedAt")] = ( + pydantic.Field(default=None) + ) + """ + Timestamp when the session was stopped (None if still active) + """ + + tasks: typing.List[TaskItemView] = pydantic.Field() + """ + List of tasks associated with this session + """ + + public_share_url: typing_extensions.Annotated[typing.Optional[str], FieldMetadata(alias="publicShareUrl")] = ( + pydantic.Field(default=None) + ) + """ + Optional URL to access the public share of the session + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/browser_use/types/share_not_found_error.py b/src/browser_use/types/share_not_found_error.py new file mode 100644 index 0000000..6f540c7 --- /dev/null +++ b/src/browser_use/types/share_not_found_error.py @@ -0,0 +1,24 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel + + +class ShareNotFoundError(UncheckedBaseModel): + """ + Error response when a public share is not found + """ + + detail: typing.Optional[str] = None + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/browser_use/types/share_view.py b/src/browser_use/types/share_view.py new file mode 100644 index 0000000..c7aee1c --- /dev/null +++ b/src/browser_use/types/share_view.py @@ -0,0 +1,47 @@ +# This file was auto-generated by Fern from our API Definition. + +import datetime as dt +import typing + +import pydantic +import typing_extensions +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.serialization import FieldMetadata +from ..core.unchecked_base_model import UncheckedBaseModel + + +class ShareView(UncheckedBaseModel): + """ + View model for representing a public share of a session. + """ + + share_token: typing_extensions.Annotated[str, FieldMetadata(alias="shareToken")] = pydantic.Field() + """ + Token to access the public share + """ + + share_url: typing_extensions.Annotated[str, FieldMetadata(alias="shareUrl")] = pydantic.Field() + """ + URL to access the public share + """ + + view_count: typing_extensions.Annotated[int, FieldMetadata(alias="viewCount")] = pydantic.Field() + """ + Number of times the public share has been viewed + """ + + last_viewed_at: typing_extensions.Annotated[typing.Optional[dt.datetime], FieldMetadata(alias="lastViewedAt")] = ( + pydantic.Field(default=None) + ) + """ + Timestamp of the last time the public share was viewed (None if never viewed) + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/browser_use/types/supported_ll_ms.py b/src/browser_use/types/supported_ll_ms.py new file mode 100644 index 0000000..980f4e4 --- /dev/null +++ b/src/browser_use/types/supported_ll_ms.py @@ -0,0 +1,20 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +SupportedLlMs = typing.Union[ + typing.Literal[ + "gpt-4.1", + "gpt-4.1-mini", + "o4-mini", + "o3", + "gemini-2.5-flash", + "gemini-2.5-pro", + "claude-sonnet-4-20250514", + "gpt-4o", + "gpt-4o-mini", + "llama-4-maverick-17b-128e-instruct", + "claude-3-7-sonnet-20250219", + ], + typing.Any, +] diff --git a/src/browser_use/types/task_created_response.py b/src/browser_use/types/task_created_response.py new file mode 100644 index 0000000..4d15a67 --- /dev/null +++ b/src/browser_use/types/task_created_response.py @@ -0,0 +1,27 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel + + +class TaskCreatedResponse(UncheckedBaseModel): + """ + Response model for creating a task + """ + + id: str = pydantic.Field() + """ + Unique identifier for the created task + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/browser_use/types/task_item_view.py b/src/browser_use/types/task_item_view.py new file mode 100644 index 0000000..54168ca --- /dev/null +++ b/src/browser_use/types/task_item_view.py @@ -0,0 +1,88 @@ +# This file was auto-generated by Fern from our API Definition. + +import datetime as dt +import typing + +import pydantic +import typing_extensions +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.serialization import FieldMetadata +from ..core.unchecked_base_model import UncheckedBaseModel +from .task_status import TaskStatus + + +class TaskItemView(UncheckedBaseModel): + """ + View model for representing a task with its execution details + """ + + id: str = pydantic.Field() + """ + Unique identifier for the task + """ + + session_id: typing_extensions.Annotated[str, FieldMetadata(alias="sessionId")] = pydantic.Field() + """ + ID of the session this task belongs to + """ + + llm: str = pydantic.Field() + """ + The LLM model used for this task represented as a string + """ + + task: str = pydantic.Field() + """ + The task prompt/instruction given to the agent + """ + + status: TaskStatus + started_at: typing_extensions.Annotated[dt.datetime, FieldMetadata(alias="startedAt")] = pydantic.Field() + """ + Naive UTC timestamp when the task was started + """ + + finished_at: typing_extensions.Annotated[typing.Optional[dt.datetime], FieldMetadata(alias="finishedAt")] = ( + pydantic.Field(default=None) + ) + """ + Naive UTC timestamp when the task completed (None if still running) + """ + + metadata: typing.Optional[typing.Dict[str, typing.Optional[typing.Any]]] = pydantic.Field(default=None) + """ + Optional additional metadata associated with the task set by the user + """ + + is_scheduled: typing_extensions.Annotated[bool, FieldMetadata(alias="isScheduled")] = pydantic.Field() + """ + Whether this task was created as a scheduled task + """ + + output: typing.Optional[str] = pydantic.Field(default=None) + """ + Final output/result of the task + """ + + browser_use_version: typing_extensions.Annotated[typing.Optional[str], FieldMetadata(alias="browserUseVersion")] = ( + pydantic.Field(default=None) + ) + """ + Version of browser-use used for this task (older tasks may not have this set) + """ + + is_success: typing_extensions.Annotated[typing.Optional[bool], FieldMetadata(alias="isSuccess")] = pydantic.Field( + default=None + ) + """ + Whether the task was successful (self-reported by the agent) + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/browser_use/types/task_list_response.py b/src/browser_use/types/task_list_response.py new file mode 100644 index 0000000..59cc4f7 --- /dev/null +++ b/src/browser_use/types/task_list_response.py @@ -0,0 +1,45 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +import typing_extensions +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.serialization import FieldMetadata +from ..core.unchecked_base_model import UncheckedBaseModel +from .task_item_view import TaskItemView + + +class TaskListResponse(UncheckedBaseModel): + """ + Response model for paginated task list requests. + """ + + items: typing.List[TaskItemView] = pydantic.Field() + """ + List of task views for the current page + """ + + total_items: typing_extensions.Annotated[int, FieldMetadata(alias="totalItems")] = pydantic.Field() + """ + Total number of items in the list + """ + + page_number: typing_extensions.Annotated[int, FieldMetadata(alias="pageNumber")] = pydantic.Field() + """ + Page number + """ + + page_size: typing_extensions.Annotated[int, FieldMetadata(alias="pageSize")] = pydantic.Field() + """ + Number of items per page + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/browser_use/types/task_log_file_response.py b/src/browser_use/types/task_log_file_response.py new file mode 100644 index 0000000..d2bff05 --- /dev/null +++ b/src/browser_use/types/task_log_file_response.py @@ -0,0 +1,29 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +import typing_extensions +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.serialization import FieldMetadata +from ..core.unchecked_base_model import UncheckedBaseModel + + +class TaskLogFileResponse(UncheckedBaseModel): + """ + Response model for log file requests + """ + + download_url: typing_extensions.Annotated[str, FieldMetadata(alias="downloadUrl")] = pydantic.Field() + """ + URL to download the log file + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/browser_use/types/task_not_found_error.py b/src/browser_use/types/task_not_found_error.py new file mode 100644 index 0000000..41c660d --- /dev/null +++ b/src/browser_use/types/task_not_found_error.py @@ -0,0 +1,24 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel + + +class TaskNotFoundError(UncheckedBaseModel): + """ + Error response when a task is not found + """ + + detail: typing.Optional[str] = None + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/browser_use/types/task_output_file_response.py b/src/browser_use/types/task_output_file_response.py new file mode 100644 index 0000000..ef5fca8 --- /dev/null +++ b/src/browser_use/types/task_output_file_response.py @@ -0,0 +1,39 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +import typing_extensions +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.serialization import FieldMetadata +from ..core.unchecked_base_model import UncheckedBaseModel + + +class TaskOutputFileResponse(UncheckedBaseModel): + """ + Response model for output file requests. + """ + + id: str = pydantic.Field() + """ + Unique identifier for the file + """ + + file_name: typing_extensions.Annotated[str, FieldMetadata(alias="fileName")] = pydantic.Field() + """ + Name of the file + """ + + download_url: typing_extensions.Annotated[str, FieldMetadata(alias="downloadUrl")] = pydantic.Field() + """ + URL to download the file + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/browser_use/types/task_status.py b/src/browser_use/types/task_status.py new file mode 100644 index 0000000..7e63a77 --- /dev/null +++ b/src/browser_use/types/task_status.py @@ -0,0 +1,5 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +TaskStatus = typing.Union[typing.Literal["started", "paused", "finished", "stopped"], typing.Any] diff --git a/src/browser_use/types/task_step_view.py b/src/browser_use/types/task_step_view.py new file mode 100644 index 0000000..aa2e957 --- /dev/null +++ b/src/browser_use/types/task_step_view.py @@ -0,0 +1,63 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +import typing_extensions +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.serialization import FieldMetadata +from ..core.unchecked_base_model import UncheckedBaseModel + + +class TaskStepView(UncheckedBaseModel): + """ + View model for representing a single step in a task's execution + """ + + number: int = pydantic.Field() + """ + Sequential step number within the task + """ + + memory: str = pydantic.Field() + """ + Agent's memory at this step + """ + + evaluation_previous_goal: typing_extensions.Annotated[str, FieldMetadata(alias="evaluationPreviousGoal")] = ( + pydantic.Field() + ) + """ + Agent's evaluation of the previous goal completion + """ + + next_goal: typing_extensions.Annotated[str, FieldMetadata(alias="nextGoal")] = pydantic.Field() + """ + The goal for the next step + """ + + url: str = pydantic.Field() + """ + Current URL the browser is on for this step + """ + + screenshot_url: typing_extensions.Annotated[typing.Optional[str], FieldMetadata(alias="screenshotUrl")] = ( + pydantic.Field(default=None) + ) + """ + Optional URL to the screenshot taken at this step + """ + + actions: typing.List[str] = pydantic.Field() + """ + List of stringified json actions performed by the agent in this step + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/browser_use/types/task_update_action.py b/src/browser_use/types/task_update_action.py new file mode 100644 index 0000000..3cd41a7 --- /dev/null +++ b/src/browser_use/types/task_update_action.py @@ -0,0 +1,5 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +TaskUpdateAction = typing.Union[typing.Literal["stop", "pause", "resume", "stop_task_and_session"], typing.Any] diff --git a/src/browser_use/types/task_view.py b/src/browser_use/types/task_view.py new file mode 100644 index 0000000..6e0ad0b --- /dev/null +++ b/src/browser_use/types/task_view.py @@ -0,0 +1,92 @@ +# This file was auto-generated by Fern from our API Definition. + +import datetime as dt +import typing + +import pydantic +import typing_extensions +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.serialization import FieldMetadata +from ..core.unchecked_base_model import UncheckedBaseModel +from .file_view import FileView +from .task_status import TaskStatus +from .task_step_view import TaskStepView + + +class TaskView(UncheckedBaseModel): + """ + View model for representing a task with its execution details + """ + + id: str = pydantic.Field() + """ + Unique identifier for the task + """ + + session_id: typing_extensions.Annotated[str, FieldMetadata(alias="sessionId")] + llm: str = pydantic.Field() + """ + The LLM model used for this task represented as a string + """ + + task: str = pydantic.Field() + """ + The task prompt/instruction given to the agent + """ + + status: TaskStatus = pydantic.Field() + """ + Current status of the task execution + """ + + started_at: typing_extensions.Annotated[dt.datetime, FieldMetadata(alias="startedAt")] = pydantic.Field() + """ + Naive UTC timestamp when the task was started + """ + + finished_at: typing_extensions.Annotated[typing.Optional[dt.datetime], FieldMetadata(alias="finishedAt")] = ( + pydantic.Field(default=None) + ) + """ + Naive UTC timestamp when the task completed (None if still running) + """ + + metadata: typing.Optional[typing.Dict[str, typing.Optional[typing.Any]]] = pydantic.Field(default=None) + """ + Optional additional metadata associated with the task set by the user + """ + + is_scheduled: typing_extensions.Annotated[bool, FieldMetadata(alias="isScheduled")] = pydantic.Field() + """ + Whether this task was created as a scheduled task + """ + + steps: typing.List[TaskStepView] + output: typing.Optional[str] = pydantic.Field(default=None) + """ + Final output/result of the task + """ + + output_files: typing_extensions.Annotated[typing.List[FileView], FieldMetadata(alias="outputFiles")] + browser_use_version: typing_extensions.Annotated[typing.Optional[str], FieldMetadata(alias="browserUseVersion")] = ( + pydantic.Field(default=None) + ) + """ + Version of browser-use used for this task (older tasks may not have this set) + """ + + is_success: typing_extensions.Annotated[typing.Optional[bool], FieldMetadata(alias="isSuccess")] = pydantic.Field( + default=None + ) + """ + Whether the task was successful (self-reported by the agent) + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/browser_use/types/unsupported_content_type_error.py b/src/browser_use/types/unsupported_content_type_error.py new file mode 100644 index 0000000..5bd09c2 --- /dev/null +++ b/src/browser_use/types/unsupported_content_type_error.py @@ -0,0 +1,24 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel + + +class UnsupportedContentTypeError(UncheckedBaseModel): + """ + Error response for unsupported content types + """ + + detail: typing.Optional[str] = None + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/browser_use/types/upload_file_presigned_url_response.py b/src/browser_use/types/upload_file_presigned_url_response.py new file mode 100644 index 0000000..8d01683 --- /dev/null +++ b/src/browser_use/types/upload_file_presigned_url_response.py @@ -0,0 +1,49 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +import typing_extensions +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.serialization import FieldMetadata +from ..core.unchecked_base_model import UncheckedBaseModel + + +class UploadFilePresignedUrlResponse(UncheckedBaseModel): + """ + Response model for a presigned upload URL. + """ + + url: str = pydantic.Field() + """ + The URL to upload the file to. + """ + + method: typing.Literal["POST"] = pydantic.Field(default="POST") + """ + The HTTP method to use for the upload. + """ + + fields: typing.Dict[str, str] = pydantic.Field() + """ + The form fields to include in the upload request. + """ + + file_name: typing_extensions.Annotated[str, FieldMetadata(alias="fileName")] = pydantic.Field() + """ + The name of the file to upload (should be referenced when user wants to use the file in a task). + """ + + expires_in: typing_extensions.Annotated[int, FieldMetadata(alias="expiresIn")] = pydantic.Field() + """ + The number of seconds until the presigned URL expires. + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/browser_use/types/validation_error.py b/src/browser_use/types/validation_error.py new file mode 100644 index 0000000..0438bc0 --- /dev/null +++ b/src/browser_use/types/validation_error.py @@ -0,0 +1,23 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel +from .validation_error_loc_item import ValidationErrorLocItem + + +class ValidationError(UncheckedBaseModel): + loc: typing.List[ValidationErrorLocItem] + msg: str + type: str + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/browser_use/types/validation_error_loc_item.py b/src/browser_use/types/validation_error_loc_item.py new file mode 100644 index 0000000..9a0a83f --- /dev/null +++ b/src/browser_use/types/validation_error_loc_item.py @@ -0,0 +1,5 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +ValidationErrorLocItem = typing.Union[str, int] diff --git a/src/browser_use/version.py b/src/browser_use/version.py new file mode 100644 index 0000000..a95c7ec --- /dev/null +++ b/src/browser_use/version.py @@ -0,0 +1,3 @@ +from importlib import metadata + +__version__ = metadata.version("browser-use") diff --git a/src/browser_use/wrapper/parse.py b/src/browser_use/wrapper/parse.py new file mode 100644 index 0000000..07303be --- /dev/null +++ b/src/browser_use/wrapper/parse.py @@ -0,0 +1,241 @@ +import asyncio +import hashlib +import json +import time +import typing +from datetime import datetime +from typing import Any, AsyncIterator, Generic, Iterator, Type, TypeVar, Union + +from pydantic import BaseModel + +from browser_use.core.request_options import RequestOptions +from browser_use.tasks.client import AsyncTasksClient, TasksClient +from browser_use.types.task_created_response import TaskCreatedResponse +from browser_use.types.task_step_view import TaskStepView +from browser_use.types.task_view import TaskView + +T = TypeVar("T", bound=BaseModel) + + +class TaskViewWithOutput(TaskView, Generic[T]): + """ + TaskView with structured output. + """ + + parsed_output: Union[T, None] + + +class CustomJSONEncoder(json.JSONEncoder): + """Custom JSON encoder to handle datetime objects.""" + + # NOTE: Python doesn't have the override decorator in 3.8, that's why we ignore it. + def default(self, o: Any) -> Any: # type: ignore[override] + if isinstance(o, datetime): + return o.isoformat() + return super().default(o) + + +def _hash_task_view(task_view: TaskView) -> str: + """Hashes the task view to detect changes.""" + return hashlib.sha256( + json.dumps(task_view.model_dump(), sort_keys=True, cls=CustomJSONEncoder).encode() + ).hexdigest() + + +def _parse_task_view_with_output(task_view: TaskView, schema: Type[T]) -> TaskViewWithOutput[T]: + """Parses the task view with output.""" + if task_view.output is None: + return TaskViewWithOutput[T](**task_view.model_dump(), parsed_output=None) + + return TaskViewWithOutput[T](**task_view.model_dump(), parsed_output=schema.model_validate_json(task_view.output)) + + +# Sync ----------------------------------------------------------------------- + + +def _watch( + client: TasksClient, task_id: str, interval: float = 1, request_options: typing.Optional[RequestOptions] = None +) -> Iterator[TaskView]: + """Yields the latest task state on every change.""" + hash: typing.Union[str, None] = None + while True: + res = client.get_task(task_id, request_options=request_options) + res_hash = _hash_task_view(res) + + if hash is None or res_hash != hash: + hash = res_hash + yield res + + if res.status == "finished" or res.status == "stopped" or res.status == "paused": + break + + time.sleep(interval) + + +def _stream( + client: TasksClient, task_id: str, interval: float = 1, request_options: typing.Optional[RequestOptions] = None +) -> Iterator[TaskStepView]: + """Streams the steps of the task and closes when the task is finished.""" + total_steps = 0 + for state in _watch(client, task_id, interval, request_options): + for i in range(total_steps, len(state.steps)): + total_steps = i + 1 + yield state.steps[i] + + +class WrappedTaskCreatedResponse(TaskCreatedResponse): + """TaskCreatedResponse with utility methods for easier interfacing with Browser Use Cloud.""" + + def __init__(self, id: str, client: TasksClient): + super().__init__(id=id) + self._client = client + + def complete(self, interval: float = 1, request_options: typing.Optional[RequestOptions] = None) -> TaskView: + """Waits for the task to finish and return the result.""" + for state in _watch(self._client, self.id, interval, request_options): + if state.status == "finished" or state.status == "stopped" or state.status == "paused": + return state + + raise Exception("Iterator ended without finding a finished state!") + + def stream( + self, interval: float = 1, request_options: typing.Optional[RequestOptions] = None + ) -> Iterator[TaskStepView]: + """Streams the steps of the task and closes when the task is finished.""" + return _stream(self._client, self.id, interval, request_options) + + def watch(self, interval: float = 1, request_options: typing.Optional[RequestOptions] = None) -> Iterator[TaskView]: + """Yields the latest task state on every change.""" + return _watch(self._client, self.id, interval, request_options) + + +# Structured + + +class WrappedStructuredTaskCreatedResponse(TaskCreatedResponse, Generic[T]): + """TaskCreatedResponse with structured output.""" + + def __init__(self, id: str, schema: Type[T], client: TasksClient): + super().__init__(id=id) + + self._client = client + self._schema = schema + + def complete( + self, interval: float = 1, request_options: typing.Optional[RequestOptions] = None + ) -> TaskViewWithOutput[T]: + """Waits for the task to finish and return the result.""" + for state in _watch(self._client, self.id, interval, request_options): + if state.status == "finished" or state.status == "stopped" or state.status == "paused": + return _parse_task_view_with_output(state, self._schema) + + raise Exception("Iterator ended without finding a finished state!") + + def stream( + self, interval: float = 1, request_options: typing.Optional[RequestOptions] = None + ) -> Iterator[TaskStepView]: + """Streams the steps of the task and closes when the task is finished.""" + return _stream(self._client, self.id, interval, request_options) + + def watch( + self, interval: float = 1, request_options: typing.Optional[RequestOptions] = None + ) -> Iterator[TaskViewWithOutput[T]]: + """Yields the latest task state on every change.""" + for state in _watch(self._client, self.id, interval, request_options): + yield _parse_task_view_with_output(state, self._schema) + + +# Async ---------------------------------------------------------------------- + + +async def _async_watch( + client: AsyncTasksClient, task_id: str, interval: float = 1, request_options: typing.Optional[RequestOptions] = None +) -> AsyncIterator[TaskView]: + """Yields the latest task state on every change.""" + hash: typing.Union[str, None] = None + while True: + res = await client.get_task(task_id, request_options=request_options) + res_hash = _hash_task_view(res) + if hash is None or res_hash != hash: + hash = res_hash + yield res + + if res.status == "finished" or res.status == "stopped" or res.status == "paused": + break + + await asyncio.sleep(interval) + + +async def _async_stream( + client: AsyncTasksClient, task_id: str, interval: float = 1, request_options: typing.Optional[RequestOptions] = None +) -> AsyncIterator[TaskStepView]: + """Streams the steps of the task and closes when the task is finished.""" + total_steps = 0 + async for state in _async_watch(client, task_id, interval, request_options): + for i in range(total_steps, len(state.steps)): + total_steps = i + 1 + yield state.steps[i] + + +class AsyncWrappedTaskCreatedResponse(TaskCreatedResponse): + """TaskCreatedResponse with utility methods for easier interfacing with Browser Use Cloud.""" + + def __init__(self, id: str, client: AsyncTasksClient): + super().__init__(id=id) + self._client = client + + async def complete(self, interval: float = 1, request_options: typing.Optional[RequestOptions] = None) -> TaskView: + """Waits for the task to finish and return the result.""" + async for state in _async_watch(self._client, self.id, interval, request_options): + if state.status == "finished" or state.status == "stopped" or state.status == "paused": + return state + + raise Exception("Iterator ended without finding a finished state!") + + def stream( + self, interval: float = 1, request_options: typing.Optional[RequestOptions] = None + ) -> AsyncIterator[TaskStepView]: + """Streams the steps of the task and closes when the task is finished.""" + return _async_stream(self._client, self.id, interval, request_options) + + async def watch( + self, interval: float = 1, request_options: typing.Optional[RequestOptions] = None + ) -> AsyncIterator[TaskView]: + """Yields the latest task state on every change.""" + return _async_watch(self._client, self.id, interval, request_options) + + +# Structured + + +class AsyncWrappedStructuredTaskCreatedResponse(TaskCreatedResponse, Generic[T]): + """TaskCreatedResponse with structured output.""" + + def __init__(self, id: str, schema: Type[T], client: AsyncTasksClient): + super().__init__(id=id) + + self._client = client + self._schema = schema + + async def complete( + self, interval: float = 1, request_options: typing.Optional[RequestOptions] = None + ) -> TaskViewWithOutput[T]: + """Waits for the task to finish and return the result.""" + async for state in _async_watch(self._client, self.id, interval, request_options): + if state.status == "finished" or state.status == "stopped" or state.status == "paused": + return _parse_task_view_with_output(state, self._schema) + + raise Exception("Iterator ended without finding a finished state!") + + def stream( + self, interval: float = 1, request_options: typing.Optional[RequestOptions] = None + ) -> AsyncIterator[TaskStepView]: + """Streams the steps of the task and closes when the task is finished.""" + return _async_stream(self._client, self.id, interval, request_options) + + async def watch( + self, interval: float = 1, request_options: typing.Optional[RequestOptions] = None + ) -> AsyncIterator[TaskViewWithOutput[T]]: + """Yields the latest task state on every change.""" + async for state in _async_watch(self._client, self.id, interval, request_options): + yield _parse_task_view_with_output(state, self._schema) diff --git a/src/browser_use/wrapper/tasks/client.py b/src/browser_use/wrapper/tasks/client.py new file mode 100644 index 0000000..835f0b8 --- /dev/null +++ b/src/browser_use/wrapper/tasks/client.py @@ -0,0 +1,284 @@ +import json +import typing + +from browser_use.core.request_options import RequestOptions +from browser_use.tasks.client import OMIT, AsyncClientWrapper, AsyncTasksClient, SyncClientWrapper, TasksClient +from browser_use.types.supported_ll_ms import SupportedLlMs +from browser_use.types.task_view import TaskView +from browser_use.wrapper.parse import ( + AsyncWrappedStructuredTaskCreatedResponse, + AsyncWrappedTaskCreatedResponse, + T, + TaskViewWithOutput, + WrappedStructuredTaskCreatedResponse, + WrappedTaskCreatedResponse, + _parse_task_view_with_output, +) + + +class BrowserUseTasksClient(TasksClient): + """TasksClient with utility method overrides.""" + + def __init__(self, *, client_wrapper: SyncClientWrapper): + super().__init__(client_wrapper=client_wrapper) + + @typing.overload + def create_task( + self, + *, + task: str, + llm: typing.Optional[SupportedLlMs] = OMIT, + start_url: typing.Optional[str] = OMIT, + max_steps: typing.Optional[int] = OMIT, + schema: typing.Type[T], + session_id: typing.Optional[str] = OMIT, + metadata: typing.Optional[typing.Dict[str, typing.Optional[str]]] = OMIT, + secrets: typing.Optional[typing.Dict[str, typing.Optional[str]]] = OMIT, + allowed_domains: typing.Optional[typing.Sequence[str]] = OMIT, + highlight_elements: typing.Optional[bool] = OMIT, + flash_mode: typing.Optional[bool] = OMIT, + thinking: typing.Optional[bool] = OMIT, + vision: typing.Optional[bool] = OMIT, + system_prompt_extension: typing.Optional[str] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> WrappedStructuredTaskCreatedResponse[T]: ... + + @typing.overload + def create_task( + self, + *, + task: str, + llm: typing.Optional[SupportedLlMs] = OMIT, + start_url: typing.Optional[str] = OMIT, + max_steps: typing.Optional[int] = OMIT, + structured_output: typing.Optional[str] = OMIT, + session_id: typing.Optional[str] = OMIT, + metadata: typing.Optional[typing.Dict[str, typing.Optional[str]]] = OMIT, + secrets: typing.Optional[typing.Dict[str, typing.Optional[str]]] = OMIT, + allowed_domains: typing.Optional[typing.Sequence[str]] = OMIT, + highlight_elements: typing.Optional[bool] = OMIT, + flash_mode: typing.Optional[bool] = OMIT, + thinking: typing.Optional[bool] = OMIT, + vision: typing.Optional[bool] = OMIT, + system_prompt_extension: typing.Optional[str] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> WrappedTaskCreatedResponse: ... + + def create_task( + self, + *, + task: str, + llm: typing.Optional[SupportedLlMs] = OMIT, + start_url: typing.Optional[str] = OMIT, + max_steps: typing.Optional[int] = OMIT, + structured_output: typing.Optional[str] = OMIT, + schema: typing.Optional[typing.Type[T]] = OMIT, + session_id: typing.Optional[str] = OMIT, + metadata: typing.Optional[typing.Dict[str, typing.Optional[str]]] = OMIT, + secrets: typing.Optional[typing.Dict[str, typing.Optional[str]]] = OMIT, + allowed_domains: typing.Optional[typing.Sequence[str]] = OMIT, + highlight_elements: typing.Optional[bool] = OMIT, + flash_mode: typing.Optional[bool] = OMIT, + thinking: typing.Optional[bool] = OMIT, + vision: typing.Optional[bool] = OMIT, + system_prompt_extension: typing.Optional[str] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> typing.Union[WrappedStructuredTaskCreatedResponse[T], WrappedTaskCreatedResponse]: + if schema is not None and schema is not OMIT: + structured_output = json.dumps(schema.model_json_schema()) + + res = super().create_task( + task=task, + llm=llm, + start_url=start_url, + max_steps=max_steps, + structured_output=structured_output, + session_id=session_id, + metadata=metadata, + secrets=secrets, + allowed_domains=allowed_domains, + highlight_elements=highlight_elements, + flash_mode=flash_mode, + thinking=thinking, + vision=vision, + system_prompt_extension=system_prompt_extension, + request_options=request_options, + ) + + return WrappedStructuredTaskCreatedResponse[T](id=res.id, schema=schema, client=self) + + else: + res = super().create_task( + task=task, + llm=llm, + start_url=start_url, + max_steps=max_steps, + structured_output=structured_output, + session_id=session_id, + metadata=metadata, + secrets=secrets, + allowed_domains=allowed_domains, + highlight_elements=highlight_elements, + flash_mode=flash_mode, + thinking=thinking, + vision=vision, + system_prompt_extension=system_prompt_extension, + request_options=request_options, + ) + + return WrappedTaskCreatedResponse(id=res.id, client=self) + + @typing.overload + def get_task( + self, task_id: str, schema: typing.Type[T], *, request_options: typing.Optional[RequestOptions] = None + ) -> TaskViewWithOutput[T]: ... + + @typing.overload + def get_task(self, task_id: str, *, request_options: typing.Optional[RequestOptions] = None) -> TaskView: ... + + def get_task( + self, + task_id: str, + schema: typing.Optional[typing.Type[T]] = OMIT, + *, + request_options: typing.Optional[RequestOptions] = None, + ) -> typing.Union[TaskViewWithOutput[T], TaskView]: + res = super().get_task(task_id, request_options=request_options) + + if schema is not None and schema is not OMIT: + return _parse_task_view_with_output(res, schema) + else: + return res + + +class AsyncBrowserUseTasksClient(AsyncTasksClient): + """AsyncTaskClient with utility method overrides.""" + + def __init__(self, *, client_wrapper: AsyncClientWrapper): + super().__init__(client_wrapper=client_wrapper) + + @typing.overload + async def create_task( + self, + *, + task: str, + llm: typing.Optional[SupportedLlMs] = OMIT, + start_url: typing.Optional[str] = OMIT, + max_steps: typing.Optional[int] = OMIT, + schema: typing.Type[T], + session_id: typing.Optional[str] = OMIT, + metadata: typing.Optional[typing.Dict[str, typing.Optional[str]]] = OMIT, + secrets: typing.Optional[typing.Dict[str, typing.Optional[str]]] = OMIT, + allowed_domains: typing.Optional[typing.Sequence[str]] = OMIT, + highlight_elements: typing.Optional[bool] = OMIT, + flash_mode: typing.Optional[bool] = OMIT, + thinking: typing.Optional[bool] = OMIT, + vision: typing.Optional[bool] = OMIT, + system_prompt_extension: typing.Optional[str] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> AsyncWrappedStructuredTaskCreatedResponse[T]: ... + + @typing.overload + async def create_task( + self, + *, + task: str, + llm: typing.Optional[SupportedLlMs] = OMIT, + start_url: typing.Optional[str] = OMIT, + max_steps: typing.Optional[int] = OMIT, + structured_output: typing.Optional[str] = OMIT, + session_id: typing.Optional[str] = OMIT, + metadata: typing.Optional[typing.Dict[str, typing.Optional[str]]] = OMIT, + secrets: typing.Optional[typing.Dict[str, typing.Optional[str]]] = OMIT, + allowed_domains: typing.Optional[typing.Sequence[str]] = OMIT, + highlight_elements: typing.Optional[bool] = OMIT, + flash_mode: typing.Optional[bool] = OMIT, + thinking: typing.Optional[bool] = OMIT, + vision: typing.Optional[bool] = OMIT, + system_prompt_extension: typing.Optional[str] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> AsyncWrappedTaskCreatedResponse: ... + + async def create_task( + self, + *, + task: str, + llm: typing.Optional[SupportedLlMs] = OMIT, + start_url: typing.Optional[str] = OMIT, + max_steps: typing.Optional[int] = OMIT, + structured_output: typing.Optional[str] = OMIT, + schema: typing.Optional[typing.Type[T]] = OMIT, + session_id: typing.Optional[str] = OMIT, + metadata: typing.Optional[typing.Dict[str, typing.Optional[str]]] = OMIT, + secrets: typing.Optional[typing.Dict[str, typing.Optional[str]]] = OMIT, + allowed_domains: typing.Optional[typing.Sequence[str]] = OMIT, + highlight_elements: typing.Optional[bool] = OMIT, + flash_mode: typing.Optional[bool] = OMIT, + thinking: typing.Optional[bool] = OMIT, + vision: typing.Optional[bool] = OMIT, + system_prompt_extension: typing.Optional[str] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> typing.Union[AsyncWrappedStructuredTaskCreatedResponse[T], AsyncWrappedTaskCreatedResponse]: + if schema is not None and schema is not OMIT: + structured_output = json.dumps(schema.model_json_schema()) + + res = await super().create_task( + task=task, + llm=llm, + start_url=start_url, + max_steps=max_steps, + structured_output=structured_output, + session_id=session_id, + metadata=metadata, + secrets=secrets, + allowed_domains=allowed_domains, + highlight_elements=highlight_elements, + flash_mode=flash_mode, + thinking=thinking, + vision=vision, + system_prompt_extension=system_prompt_extension, + request_options=request_options, + ) + return AsyncWrappedStructuredTaskCreatedResponse[T](id=res.id, schema=schema, client=self) + + else: + res = await super().create_task( + task=task, + llm=llm, + start_url=start_url, + max_steps=max_steps, + structured_output=structured_output, + session_id=session_id, + metadata=metadata, + secrets=secrets, + allowed_domains=allowed_domains, + highlight_elements=highlight_elements, + flash_mode=flash_mode, + thinking=thinking, + vision=vision, + system_prompt_extension=system_prompt_extension, + request_options=request_options, + ) + return AsyncWrappedTaskCreatedResponse(id=res.id, client=self) + + @typing.overload + async def get_task( + self, task_id: str, schema: typing.Type[T], *, request_options: typing.Optional[RequestOptions] = None + ) -> TaskViewWithOutput[T]: ... + + @typing.overload + async def get_task(self, task_id: str, *, request_options: typing.Optional[RequestOptions] = None) -> TaskView: ... + + async def get_task( + self, + task_id: str, + schema: typing.Optional[typing.Type[T]] = OMIT, + *, + request_options: typing.Optional[RequestOptions] = None, + ) -> typing.Union[TaskViewWithOutput[T], TaskView]: + res = await super().get_task(task_id, request_options=request_options) + + if schema is not None and schema is not OMIT: + return _parse_task_view_with_output(res, schema) + else: + return res diff --git a/src/browser_use_sdk/__init__.py b/src/browser_use_sdk/__init__.py deleted file mode 100644 index 235c0b0..0000000 --- a/src/browser_use_sdk/__init__.py +++ /dev/null @@ -1,100 +0,0 @@ -# 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 -from ._client import ( - Client, - Stream, - Timeout, - Transport, - BrowserUse, - AsyncClient, - AsyncStream, - RequestOptions, - AsyncBrowserUse, -) -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, - BrowserUseError, - APIConnectionError, - AuthenticationError, - InternalServerError, - PermissionDeniedError, - UnprocessableEntityError, - APIResponseValidationError, -) -from ._base_client import DefaultHttpxClient, DefaultAioHttpClient, DefaultAsyncHttpxClient -from ._utils._logs import setup_logging as _setup_logging - -__all__ = [ - "types", - "__version__", - "__title__", - "NoneType", - "Transport", - "ProxiesTypes", - "NotGiven", - "NOT_GIVEN", - "Omit", - "BrowserUseError", - "APIError", - "APIStatusError", - "APITimeoutError", - "APIConnectionError", - "APIResponseValidationError", - "BadRequestError", - "AuthenticationError", - "PermissionDeniedError", - "NotFoundError", - "ConflictError", - "UnprocessableEntityError", - "RateLimitError", - "InternalServerError", - "Timeout", - "RequestOptions", - "Client", - "AsyncClient", - "Stream", - "AsyncStream", - "BrowserUse", - "AsyncBrowserUse", - "file_from_path", - "BaseModel", - "DEFAULT_TIMEOUT", - "DEFAULT_MAX_RETRIES", - "DEFAULT_CONNECTION_LIMITS", - "DefaultHttpxClient", - "DefaultAsyncHttpxClient", - "DefaultAioHttpClient", -] - -if not _t.TYPE_CHECKING: - from ._utils._resources_proxy import resources as resources - -_setup_logging() - -# Update the __module__ attribute for exported symbols so that -# error messages point to this module instead of the module -# it was originally defined in, e.g. -# browser_use_sdk._exceptions.NotFoundError -> browser_use_sdk.NotFoundError -__locals = locals() -for __name in __all__: - if not __name.startswith("__"): - try: - __locals[__name].__module__ = "browser_use_sdk" - except (TypeError, AttributeError): - # Some of our exported symbols are builtins which we can't set attributes for. - pass diff --git a/src/browser_use_sdk/_base_client.py b/src/browser_use_sdk/_base_client.py deleted file mode 100644 index f182716..0000000 --- a/src/browser_use_sdk/_base_client.py +++ /dev/null @@ -1,1995 +0,0 @@ -from __future__ import annotations - -import sys -import json -import time -import uuid -import email -import asyncio -import inspect -import logging -import platform -import email.utils -from types import TracebackType -from random import random -from typing import ( - TYPE_CHECKING, - Any, - Dict, - Type, - Union, - Generic, - Mapping, - TypeVar, - Iterable, - Iterator, - Optional, - Generator, - AsyncIterator, - cast, - overload, -) -from typing_extensions import Literal, override, get_origin - -import anyio -import httpx -import distro -import pydantic -from httpx import URL -from pydantic import PrivateAttr - -from . import _exceptions -from ._qs import Querystring -from ._files import to_httpx_files, async_to_httpx_files -from ._types import ( - NOT_GIVEN, - Body, - Omit, - Query, - Headers, - Timeout, - NotGiven, - ResponseT, - AnyMapping, - PostParser, - RequestFiles, - HttpxSendArgs, - RequestOptions, - HttpxRequestFiles, - 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 ._models import GenericModel, FinalRequestOptions, validate_type, construct_type -from ._response import ( - APIResponse, - BaseAPIResponse, - AsyncAPIResponse, - extract_response_type, -) -from ._constants import ( - DEFAULT_TIMEOUT, - MAX_RETRY_DELAY, - DEFAULT_MAX_RETRIES, - INITIAL_RETRY_DELAY, - RAW_RESPONSE_HEADER, - OVERRIDE_CAST_TO_HEADER, - DEFAULT_CONNECTION_LIMITS, -) -from ._streaming import Stream, SSEDecoder, AsyncStream, SSEBytesDecoder -from ._exceptions import ( - APIStatusError, - APITimeoutError, - APIConnectionError, - APIResponseValidationError, -) - -log: logging.Logger = logging.getLogger(__name__) - -# TODO: make base page type vars covariant -SyncPageT = TypeVar("SyncPageT", bound="BaseSyncPage[Any]") -AsyncPageT = TypeVar("AsyncPageT", bound="BaseAsyncPage[Any]") - - -_T = TypeVar("_T") -_T_co = TypeVar("_T_co", covariant=True) - -_StreamT = TypeVar("_StreamT", bound=Stream[Any]) -_AsyncStreamT = TypeVar("_AsyncStreamT", bound=AsyncStream[Any]) - -if TYPE_CHECKING: - from httpx._config import ( - DEFAULT_TIMEOUT_CONFIG, # pyright: ignore[reportPrivateImportUsage] - ) - - HTTPX_DEFAULT_TIMEOUT = DEFAULT_TIMEOUT_CONFIG -else: - try: - from httpx._config import DEFAULT_TIMEOUT_CONFIG as HTTPX_DEFAULT_TIMEOUT - except ImportError: - # taken from https://github.com/encode/httpx/blob/3ba5fe0d7ac70222590e759c31442b1cab263791/httpx/_config.py#L366 - HTTPX_DEFAULT_TIMEOUT = Timeout(5.0) - - -class PageInfo: - """Stores the necessary information to build the request to retrieve the next page. - - Either `url` or `params` must be set. - """ - - url: URL | NotGiven - params: Query | NotGiven - json: Body | NotGiven - - @overload - def __init__( - self, - *, - url: URL, - ) -> None: ... - - @overload - def __init__( - self, - *, - params: Query, - ) -> None: ... - - @overload - def __init__( - self, - *, - json: Body, - ) -> None: ... - - def __init__( - self, - *, - url: URL | NotGiven = NOT_GIVEN, - json: Body | NotGiven = NOT_GIVEN, - params: Query | NotGiven = NOT_GIVEN, - ) -> None: - self.url = url - self.json = json - self.params = params - - @override - def __repr__(self) -> str: - if self.url: - return f"{self.__class__.__name__}(url={self.url})" - if self.json: - return f"{self.__class__.__name__}(json={self.json})" - return f"{self.__class__.__name__}(params={self.params})" - - -class BasePage(GenericModel, Generic[_T]): - """ - Defines the core interface for pagination. - - Type Args: - ModelT: The pydantic model that represents an item in the response. - - Methods: - has_next_page(): Check if there is another page available - next_page_info(): Get the necessary information to make a request for the next page - """ - - _options: FinalRequestOptions = PrivateAttr() - _model: Type[_T] = PrivateAttr() - - def has_next_page(self) -> bool: - items = self._get_page_items() - if not items: - return False - return self.next_page_info() is not None - - def next_page_info(self) -> Optional[PageInfo]: ... - - def _get_page_items(self) -> Iterable[_T]: # type: ignore[empty-body] - ... - - def _params_from_url(self, url: URL) -> httpx.QueryParams: - # TODO: do we have to preprocess params here? - return httpx.QueryParams(cast(Any, self._options.params)).merge(url.params) - - def _info_to_options(self, info: PageInfo) -> FinalRequestOptions: - options = model_copy(self._options) - options._strip_raw_response_header() - - if not isinstance(info.params, NotGiven): - options.params = {**options.params, **info.params} - return options - - if not isinstance(info.url, NotGiven): - params = self._params_from_url(info.url) - url = info.url.copy_with(params=params) - options.params = dict(url.params) - options.url = str(url) - return options - - if not isinstance(info.json, NotGiven): - if not is_mapping(info.json): - raise TypeError("Pagination is only supported with mappings") - - if not options.json_data: - options.json_data = {**info.json} - else: - if not is_mapping(options.json_data): - raise TypeError("Pagination is only supported with mappings") - - options.json_data = {**options.json_data, **info.json} - return options - - raise ValueError("Unexpected PageInfo state") - - -class BaseSyncPage(BasePage[_T], Generic[_T]): - _client: SyncAPIClient = pydantic.PrivateAttr() - - def _set_private_attributes( - self, - client: SyncAPIClient, - model: Type[_T], - options: FinalRequestOptions, - ) -> None: - if PYDANTIC_V2 and getattr(self, "__pydantic_private__", None) is None: - self.__pydantic_private__ = {} - - self._model = model - self._client = client - self._options = options - - # Pydantic uses a custom `__iter__` method to support casting BaseModels - # to dictionaries. e.g. dict(model). - # As we want to support `for item in page`, this is inherently incompatible - # with the default pydantic behaviour. It is not possible to support both - # use cases at once. Fortunately, this is not a big deal as all other pydantic - # methods should continue to work as expected as there is an alternative method - # to cast a model to a dictionary, model.dict(), which is used internally - # by pydantic. - def __iter__(self) -> Iterator[_T]: # type: ignore - for page in self.iter_pages(): - for item in page._get_page_items(): - yield item - - def iter_pages(self: SyncPageT) -> Iterator[SyncPageT]: - page = self - while True: - yield page - if page.has_next_page(): - page = page.get_next_page() - else: - return - - def get_next_page(self: SyncPageT) -> SyncPageT: - info = self.next_page_info() - if not info: - raise RuntimeError( - "No next page expected; please check `.has_next_page()` before calling `.get_next_page()`." - ) - - options = self._info_to_options(info) - return self._client._request_api_list(self._model, page=self.__class__, options=options) - - -class AsyncPaginator(Generic[_T, AsyncPageT]): - def __init__( - self, - client: AsyncAPIClient, - options: FinalRequestOptions, - page_cls: Type[AsyncPageT], - model: Type[_T], - ) -> None: - self._model = model - self._client = client - self._options = options - self._page_cls = page_cls - - def __await__(self) -> Generator[Any, None, AsyncPageT]: - return self._get_page().__await__() - - async def _get_page(self) -> AsyncPageT: - def _parser(resp: AsyncPageT) -> AsyncPageT: - resp._set_private_attributes( - model=self._model, - options=self._options, - client=self._client, - ) - return resp - - self._options.post_parser = _parser - - return await self._client.request(self._page_cls, self._options) - - async def __aiter__(self) -> AsyncIterator[_T]: - # https://github.com/microsoft/pyright/issues/3464 - page = cast( - AsyncPageT, - await self, # type: ignore - ) - async for item in page: - yield item - - -class BaseAsyncPage(BasePage[_T], Generic[_T]): - _client: AsyncAPIClient = pydantic.PrivateAttr() - - def _set_private_attributes( - self, - model: Type[_T], - client: AsyncAPIClient, - options: FinalRequestOptions, - ) -> None: - if PYDANTIC_V2 and getattr(self, "__pydantic_private__", None) is None: - self.__pydantic_private__ = {} - - self._model = model - self._client = client - self._options = options - - async def __aiter__(self) -> AsyncIterator[_T]: - async for page in self.iter_pages(): - for item in page._get_page_items(): - yield item - - async def iter_pages(self: AsyncPageT) -> AsyncIterator[AsyncPageT]: - page = self - while True: - yield page - if page.has_next_page(): - page = await page.get_next_page() - else: - return - - async def get_next_page(self: AsyncPageT) -> AsyncPageT: - info = self.next_page_info() - if not info: - raise RuntimeError( - "No next page expected; please check `.has_next_page()` before calling `.get_next_page()`." - ) - - options = self._info_to_options(info) - return await self._client._request_api_list(self._model, page=self.__class__, options=options) - - -_HttpxClientT = TypeVar("_HttpxClientT", bound=Union[httpx.Client, httpx.AsyncClient]) -_DefaultStreamT = TypeVar("_DefaultStreamT", bound=Union[Stream[Any], AsyncStream[Any]]) - - -class BaseClient(Generic[_HttpxClientT, _DefaultStreamT]): - _client: _HttpxClientT - _version: str - _base_url: URL - max_retries: int - timeout: Union[float, Timeout, None] - _strict_response_validation: bool - _idempotency_header: str | None - _default_stream_cls: type[_DefaultStreamT] | None = None - - def __init__( - self, - *, - version: str, - base_url: str | URL, - _strict_response_validation: bool, - max_retries: int = DEFAULT_MAX_RETRIES, - timeout: float | Timeout | None = DEFAULT_TIMEOUT, - custom_headers: Mapping[str, str] | None = None, - custom_query: Mapping[str, object] | None = None, - ) -> None: - self._version = version - self._base_url = self._enforce_trailing_slash(URL(base_url)) - self.max_retries = max_retries - self.timeout = timeout - self._custom_headers = custom_headers or {} - self._custom_query = custom_query or {} - self._strict_response_validation = _strict_response_validation - self._idempotency_header = None - self._platform: Platform | None = None - - if max_retries is None: # pyright: ignore[reportUnnecessaryComparison] - raise TypeError( - "max_retries cannot be None. If you want to disable retries, pass `0`; if you want unlimited retries, pass `math.inf` or a very high number; if you want the default behavior, pass `browser_use_sdk.DEFAULT_MAX_RETRIES`" - ) - - def _enforce_trailing_slash(self, url: URL) -> URL: - if url.raw_path.endswith(b"/"): - return url - return url.copy_with(raw_path=url.raw_path + b"/") - - def _make_status_error_from_response( - self, - response: httpx.Response, - ) -> APIStatusError: - if response.is_closed and not response.is_stream_consumed: - # We can't read the response body as it has been closed - # before it was read. This can happen if an event hook - # raises a status error. - body = None - err_msg = f"Error code: {response.status_code}" - else: - err_text = response.text.strip() - body = err_text - - try: - body = json.loads(err_text) - err_msg = f"Error code: {response.status_code} - {body}" - except Exception: - err_msg = err_text or f"Error code: {response.status_code}" - - return self._make_status_error(err_msg, body=body, response=response) - - def _make_status_error( - self, - err_msg: str, - *, - body: object, - response: httpx.Response, - ) -> _exceptions.APIStatusError: - raise NotImplementedError() - - def _build_headers(self, options: FinalRequestOptions, *, retries_taken: int = 0) -> httpx.Headers: - custom_headers = options.headers or {} - headers_dict = _merge_mappings(self.default_headers, custom_headers) - self._validate_headers(headers_dict, custom_headers) - - # headers are case-insensitive while dictionaries are not. - headers = httpx.Headers(headers_dict) - - idempotency_header = self._idempotency_header - if idempotency_header and options.idempotency_key and idempotency_header not in headers: - headers[idempotency_header] = options.idempotency_key - - # Don't set these headers if they were already set or removed by the caller. We check - # `custom_headers`, which can contain `Omit()`, instead of `headers` to account for the removal case. - lower_custom_headers = [header.lower() for header in custom_headers] - if "x-stainless-retry-count" not in lower_custom_headers: - headers["x-stainless-retry-count"] = str(retries_taken) - if "x-stainless-read-timeout" not in lower_custom_headers: - timeout = self.timeout if isinstance(options.timeout, NotGiven) else options.timeout - if isinstance(timeout, Timeout): - timeout = timeout.read - if timeout is not None: - headers["x-stainless-read-timeout"] = str(timeout) - - return headers - - def _prepare_url(self, url: str) -> URL: - """ - Merge a URL argument together with any 'base_url' on the client, - to create the URL used for the outgoing request. - """ - # Copied from httpx's `_merge_url` method. - merge_url = URL(url) - if merge_url.is_relative_url: - merge_raw_path = self.base_url.raw_path + merge_url.raw_path.lstrip(b"/") - return self.base_url.copy_with(raw_path=merge_raw_path) - - return merge_url - - def _make_sse_decoder(self) -> SSEDecoder | SSEBytesDecoder: - return SSEDecoder() - - def _build_request( - self, - options: FinalRequestOptions, - *, - retries_taken: int = 0, - ) -> httpx.Request: - if log.isEnabledFor(logging.DEBUG): - log.debug("Request options: %s", model_dump(options, exclude_unset=True)) - - kwargs: dict[str, Any] = {} - - json_data = options.json_data - if options.extra_json is not None: - if json_data is None: - json_data = cast(Body, options.extra_json) - elif is_mapping(json_data): - json_data = _merge_mappings(json_data, options.extra_json) - else: - raise RuntimeError(f"Unexpected JSON data type, {type(json_data)}, cannot merge with `extra_body`") - - headers = self._build_headers(options, retries_taken=retries_taken) - params = _merge_mappings(self.default_query, options.params) - content_type = headers.get("Content-Type") - files = options.files - - # If the given Content-Type header is multipart/form-data then it - # has to be removed so that httpx can generate the header with - # additional information for us as it has to be in this form - # for the server to be able to correctly parse the request: - # multipart/form-data; boundary=---abc-- - if content_type is not None and content_type.startswith("multipart/form-data"): - if "boundary" not in content_type: - # only remove the header if the boundary hasn't been explicitly set - # as the caller doesn't want httpx to come up with their own boundary - headers.pop("Content-Type") - - # As we are now sending multipart/form-data instead of application/json - # we need to tell httpx to use it, https://www.python-httpx.org/advanced/clients/#multipart-file-encoding - if json_data: - if not is_dict(json_data): - raise TypeError( - f"Expected query input to be a dictionary for multipart requests but got {type(json_data)} instead." - ) - kwargs["data"] = self._serialize_multipartform(json_data) - - # httpx determines whether or not to send a "multipart/form-data" - # request based on the truthiness of the "files" argument. - # This gets around that issue by generating a dict value that - # evaluates to true. - # - # https://github.com/encode/httpx/discussions/2399#discussioncomment-3814186 - if not files: - files = cast(HttpxRequestFiles, ForceMultipartDict()) - - prepared_url = self._prepare_url(options.url) - if "_" in prepared_url.host: - # work around https://github.com/encode/httpx/discussions/2880 - kwargs["extensions"] = {"sni_hostname": prepared_url.host.replace("_", "-")} - - is_body_allowed = options.method.lower() != "get" - - if is_body_allowed: - if isinstance(json_data, bytes): - kwargs["content"] = json_data - else: - kwargs["json"] = json_data if is_given(json_data) else None - kwargs["files"] = files - else: - headers.pop("Content-Type", None) - kwargs.pop("data", None) - - # TODO: report this error to httpx - return self._client.build_request( # pyright: ignore[reportUnknownMemberType] - headers=headers, - timeout=self.timeout if isinstance(options.timeout, NotGiven) else options.timeout, - method=options.method, - url=prepared_url, - # the `Query` type that we use is incompatible with qs' - # `Params` type as it needs to be typed as `Mapping[str, object]` - # so that passing a `TypedDict` doesn't cause an error. - # https://github.com/microsoft/pyright/issues/3526#event-6715453066 - params=self.qs.stringify(cast(Mapping[str, Any], params)) if params else None, - **kwargs, - ) - - def _serialize_multipartform(self, data: Mapping[object, object]) -> dict[str, object]: - items = self.qs.stringify_items( - # TODO: type ignore is required as stringify_items is well typed but we can't be - # well typed without heavy validation. - data, # type: ignore - array_format="brackets", - ) - serialized: dict[str, object] = {} - for key, value in items: - existing = serialized.get(key) - - if not existing: - serialized[key] = value - continue - - # If a value has already been set for this key then that - # means we're sending data like `array[]=[1, 2, 3]` and we - # need to tell httpx that we want to send multiple values with - # the same key which is done by using a list or a tuple. - # - # Note: 2d arrays should never result in the same key at both - # levels so it's safe to assume that if the value is a list, - # it was because we changed it to be a list. - if is_list(existing): - existing.append(value) - else: - serialized[key] = [existing, value] - - return serialized - - def _maybe_override_cast_to(self, cast_to: type[ResponseT], options: FinalRequestOptions) -> type[ResponseT]: - if not is_given(options.headers): - return cast_to - - # make a copy of the headers so we don't mutate user-input - headers = dict(options.headers) - - # we internally support defining a temporary header to override the - # default `cast_to` type for use with `.with_raw_response` and `.with_streaming_response` - # see _response.py for implementation details - override_cast_to = headers.pop(OVERRIDE_CAST_TO_HEADER, NOT_GIVEN) - if is_given(override_cast_to): - options.headers = headers - return cast(Type[ResponseT], override_cast_to) - - return cast_to - - def _should_stream_response_body(self, request: httpx.Request) -> bool: - return request.headers.get(RAW_RESPONSE_HEADER) == "stream" # type: ignore[no-any-return] - - def _process_response_data( - self, - *, - data: object, - cast_to: type[ResponseT], - response: httpx.Response, - ) -> ResponseT: - if data is None: - return cast(ResponseT, None) - - if cast_to is object: - return cast(ResponseT, data) - - try: - if inspect.isclass(cast_to) and issubclass(cast_to, ModelBuilderProtocol): - return cast(ResponseT, cast_to.build(response=response, data=data)) - - if self._strict_response_validation: - return cast(ResponseT, validate_type(type_=cast_to, value=data)) - - return cast(ResponseT, construct_type(type_=cast_to, value=data)) - except pydantic.ValidationError as err: - raise APIResponseValidationError(response=response, body=data) from err - - @property - def qs(self) -> Querystring: - return Querystring() - - @property - def custom_auth(self) -> httpx.Auth | None: - return None - - @property - def auth_headers(self) -> dict[str, str]: - return {} - - @property - def default_headers(self) -> dict[str, str | Omit]: - return { - "Accept": "application/json", - "Content-Type": "application/json", - "User-Agent": self.user_agent, - **self.platform_headers(), - **self.auth_headers, - **self._custom_headers, - } - - @property - def default_query(self) -> dict[str, object]: - return { - **self._custom_query, - } - - def _validate_headers( - self, - headers: Headers, # noqa: ARG002 - custom_headers: Headers, # noqa: ARG002 - ) -> None: - """Validate the given default headers and custom headers. - - Does nothing by default. - """ - return - - @property - def user_agent(self) -> str: - return f"{self.__class__.__name__}/Python {self._version}" - - @property - def base_url(self) -> URL: - return self._base_url - - @base_url.setter - def base_url(self, url: URL | str) -> None: - self._base_url = self._enforce_trailing_slash(url if isinstance(url, URL) else URL(url)) - - def platform_headers(self) -> Dict[str, str]: - # the actual implementation is in a separate `lru_cache` decorated - # function because adding `lru_cache` to methods will leak memory - # https://github.com/python/cpython/issues/88476 - return platform_headers(self._version, platform=self._platform) - - def _parse_retry_after_header(self, response_headers: Optional[httpx.Headers] = None) -> float | None: - """Returns a float of the number of seconds (not milliseconds) to wait after retrying, or None if unspecified. - - About the Retry-After header: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After - See also https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After#syntax - """ - if response_headers is None: - return None - - # First, try the non-standard `retry-after-ms` header for milliseconds, - # which is more precise than integer-seconds `retry-after` - try: - retry_ms_header = response_headers.get("retry-after-ms", None) - return float(retry_ms_header) / 1000 - except (TypeError, ValueError): - pass - - # Next, try parsing `retry-after` header as seconds (allowing nonstandard floats). - retry_header = response_headers.get("retry-after") - try: - # note: the spec indicates that this should only ever be an integer - # but if someone sends a float there's no reason for us to not respect it - return float(retry_header) - except (TypeError, ValueError): - pass - - # Last, try parsing `retry-after` as a date. - retry_date_tuple = email.utils.parsedate_tz(retry_header) - if retry_date_tuple is None: - return None - - retry_date = email.utils.mktime_tz(retry_date_tuple) - return float(retry_date - time.time()) - - def _calculate_retry_timeout( - self, - remaining_retries: int, - options: FinalRequestOptions, - response_headers: Optional[httpx.Headers] = None, - ) -> float: - max_retries = options.get_max_retries(self.max_retries) - - # If the API asks us to wait a certain amount of time (and it's a reasonable amount), just do what it says. - retry_after = self._parse_retry_after_header(response_headers) - if retry_after is not None and 0 < retry_after <= 60: - return retry_after - - # Also cap retry count to 1000 to avoid any potential overflows with `pow` - nb_retries = min(max_retries - remaining_retries, 1000) - - # Apply exponential backoff, but not more than the max. - sleep_seconds = min(INITIAL_RETRY_DELAY * pow(2.0, nb_retries), MAX_RETRY_DELAY) - - # Apply some jitter, plus-or-minus half a second. - jitter = 1 - 0.25 * random() - timeout = sleep_seconds * jitter - return timeout if timeout >= 0 else 0 - - def _should_retry(self, response: httpx.Response) -> bool: - # Note: this is not a standard header - should_retry_header = response.headers.get("x-should-retry") - - # If the server explicitly says whether or not to retry, obey. - if should_retry_header == "true": - log.debug("Retrying as header `x-should-retry` is set to `true`") - return True - if should_retry_header == "false": - log.debug("Not retrying as header `x-should-retry` is set to `false`") - return False - - # Retry on request timeouts. - if response.status_code == 408: - log.debug("Retrying due to status code %i", response.status_code) - return True - - # Retry on lock timeouts. - if response.status_code == 409: - log.debug("Retrying due to status code %i", response.status_code) - return True - - # Retry on rate limits. - if response.status_code == 429: - log.debug("Retrying due to status code %i", response.status_code) - return True - - # Retry internal errors. - if response.status_code >= 500: - log.debug("Retrying due to status code %i", response.status_code) - return True - - log.debug("Not retrying") - return False - - def _idempotency_key(self) -> str: - return f"stainless-python-retry-{uuid.uuid4()}" - - -class _DefaultHttpxClient(httpx.Client): - def __init__(self, **kwargs: Any) -> None: - kwargs.setdefault("timeout", DEFAULT_TIMEOUT) - kwargs.setdefault("limits", DEFAULT_CONNECTION_LIMITS) - kwargs.setdefault("follow_redirects", True) - super().__init__(**kwargs) - - -if TYPE_CHECKING: - DefaultHttpxClient = httpx.Client - """An alias to `httpx.Client` that provides the same defaults that this SDK - uses internally. - - This is useful because overriding the `http_client` with your own instance of - `httpx.Client` will result in httpx's defaults being used, not ours. - """ -else: - DefaultHttpxClient = _DefaultHttpxClient - - -class SyncHttpxClientWrapper(DefaultHttpxClient): - def __del__(self) -> None: - if self.is_closed: - return - - try: - self.close() - except Exception: - pass - - -class SyncAPIClient(BaseClient[httpx.Client, Stream[Any]]): - _client: httpx.Client - _default_stream_cls: type[Stream[Any]] | None = None - - def __init__( - self, - *, - version: str, - base_url: str | URL, - max_retries: int = DEFAULT_MAX_RETRIES, - timeout: float | Timeout | None | NotGiven = NOT_GIVEN, - http_client: httpx.Client | None = None, - custom_headers: Mapping[str, str] | None = None, - custom_query: Mapping[str, object] | None = None, - _strict_response_validation: bool, - ) -> None: - if not is_given(timeout): - # if the user passed in a custom http client with a non-default - # timeout set then we use that timeout. - # - # note: there is an edge case here where the user passes in a client - # where they've explicitly set the timeout to match the default timeout - # as this check is structural, meaning that we'll think they didn't - # pass in a timeout and will ignore it - if http_client and http_client.timeout != HTTPX_DEFAULT_TIMEOUT: - timeout = http_client.timeout - else: - timeout = DEFAULT_TIMEOUT - - if http_client is not None and not isinstance(http_client, httpx.Client): # pyright: ignore[reportUnnecessaryIsInstance] - raise TypeError( - f"Invalid `http_client` argument; Expected an instance of `httpx.Client` but got {type(http_client)}" - ) - - super().__init__( - version=version, - # cast to a valid type because mypy doesn't understand our type narrowing - timeout=cast(Timeout, timeout), - base_url=base_url, - max_retries=max_retries, - custom_query=custom_query, - custom_headers=custom_headers, - _strict_response_validation=_strict_response_validation, - ) - self._client = http_client or SyncHttpxClientWrapper( - base_url=base_url, - # cast to a valid type because mypy doesn't understand our type narrowing - timeout=cast(Timeout, timeout), - ) - - def is_closed(self) -> bool: - return self._client.is_closed - - def close(self) -> None: - """Close the underlying HTTPX client. - - The client will *not* be usable after this. - """ - # If an error is thrown while constructing a client, self._client - # may not be present - if hasattr(self, "_client"): - self._client.close() - - def __enter__(self: _T) -> _T: - return self - - def __exit__( - self, - exc_type: type[BaseException] | None, - exc: BaseException | None, - exc_tb: TracebackType | None, - ) -> None: - self.close() - - def _prepare_options( - self, - options: FinalRequestOptions, # noqa: ARG002 - ) -> FinalRequestOptions: - """Hook for mutating the given options""" - return options - - def _prepare_request( - self, - request: httpx.Request, # noqa: ARG002 - ) -> None: - """This method is used as a callback for mutating the `Request` object - after it has been constructed. - This is useful for cases where you want to add certain headers based off of - the request properties, e.g. `url`, `method` etc. - """ - return None - - @overload - def request( - self, - cast_to: Type[ResponseT], - options: FinalRequestOptions, - *, - stream: Literal[True], - stream_cls: Type[_StreamT], - ) -> _StreamT: ... - - @overload - def request( - self, - cast_to: Type[ResponseT], - options: FinalRequestOptions, - *, - stream: Literal[False] = False, - ) -> ResponseT: ... - - @overload - def request( - self, - cast_to: Type[ResponseT], - options: FinalRequestOptions, - *, - stream: bool = False, - stream_cls: Type[_StreamT] | None = None, - ) -> ResponseT | _StreamT: ... - - def request( - self, - cast_to: Type[ResponseT], - options: FinalRequestOptions, - *, - stream: bool = False, - stream_cls: type[_StreamT] | None = None, - ) -> ResponseT | _StreamT: - cast_to = self._maybe_override_cast_to(cast_to, options) - - # create a copy of the options we were given so that if the - # options are mutated later & we then retry, the retries are - # given the original options - input_options = model_copy(options) - if input_options.idempotency_key is None and input_options.method.lower() != "get": - # ensure the idempotency key is reused between requests - input_options.idempotency_key = self._idempotency_key() - - response: httpx.Response | None = None - max_retries = input_options.get_max_retries(self.max_retries) - - retries_taken = 0 - for retries_taken in range(max_retries + 1): - options = model_copy(input_options) - options = self._prepare_options(options) - - remaining_retries = max_retries - retries_taken - request = self._build_request(options, retries_taken=retries_taken) - self._prepare_request(request) - - kwargs: HttpxSendArgs = {} - if self.custom_auth is not None: - kwargs["auth"] = self.custom_auth - - if options.follow_redirects is not None: - kwargs["follow_redirects"] = options.follow_redirects - - log.debug("Sending HTTP Request: %s %s", request.method, request.url) - - response = None - try: - response = self._client.send( - request, - stream=stream or self._should_stream_response_body(request=request), - **kwargs, - ) - except httpx.TimeoutException as err: - log.debug("Encountered httpx.TimeoutException", exc_info=True) - - if remaining_retries > 0: - self._sleep_for_retry( - retries_taken=retries_taken, - max_retries=max_retries, - options=input_options, - response=None, - ) - continue - - log.debug("Raising timeout error") - raise APITimeoutError(request=request) from err - except Exception as err: - log.debug("Encountered Exception", exc_info=True) - - if remaining_retries > 0: - self._sleep_for_retry( - retries_taken=retries_taken, - max_retries=max_retries, - options=input_options, - response=None, - ) - continue - - log.debug("Raising connection error") - raise APIConnectionError(request=request) from err - - log.debug( - 'HTTP Response: %s %s "%i %s" %s', - request.method, - request.url, - response.status_code, - response.reason_phrase, - response.headers, - ) - - try: - response.raise_for_status() - except httpx.HTTPStatusError as err: # thrown on 4xx and 5xx status code - log.debug("Encountered httpx.HTTPStatusError", exc_info=True) - - if remaining_retries > 0 and self._should_retry(err.response): - err.response.close() - self._sleep_for_retry( - retries_taken=retries_taken, - max_retries=max_retries, - options=input_options, - response=response, - ) - continue - - # If the response is streamed then we need to explicitly read the response - # to completion before attempting to access the response text. - if not err.response.is_closed: - err.response.read() - - log.debug("Re-raising status error") - raise self._make_status_error_from_response(err.response) from None - - break - - assert response is not None, "could not resolve response (should never happen)" - return self._process_response( - cast_to=cast_to, - options=options, - response=response, - stream=stream, - stream_cls=stream_cls, - retries_taken=retries_taken, - ) - - def _sleep_for_retry( - self, *, retries_taken: int, max_retries: int, options: FinalRequestOptions, response: httpx.Response | None - ) -> None: - remaining_retries = max_retries - retries_taken - if remaining_retries == 1: - log.debug("1 retry left") - else: - log.debug("%i retries left", remaining_retries) - - timeout = self._calculate_retry_timeout(remaining_retries, options, response.headers if response else None) - log.info("Retrying request to %s in %f seconds", options.url, timeout) - - time.sleep(timeout) - - def _process_response( - self, - *, - cast_to: Type[ResponseT], - options: FinalRequestOptions, - response: httpx.Response, - stream: bool, - stream_cls: type[Stream[Any]] | type[AsyncStream[Any]] | None, - retries_taken: int = 0, - ) -> ResponseT: - origin = get_origin(cast_to) or cast_to - - if ( - inspect.isclass(origin) - and issubclass(origin, BaseAPIResponse) - # we only want to actually return the custom BaseAPIResponse class if we're - # returning the raw response, or if we're not streaming SSE, as if we're streaming - # SSE then `cast_to` doesn't actively reflect the type we need to parse into - and (not stream or bool(response.request.headers.get(RAW_RESPONSE_HEADER))) - ): - if not issubclass(origin, APIResponse): - raise TypeError(f"API Response types must subclass {APIResponse}; Received {origin}") - - response_cls = cast("type[BaseAPIResponse[Any]]", cast_to) - return cast( - ResponseT, - response_cls( - raw=response, - client=self, - cast_to=extract_response_type(response_cls), - stream=stream, - stream_cls=stream_cls, - options=options, - retries_taken=retries_taken, - ), - ) - - if cast_to == httpx.Response: - return cast(ResponseT, response) - - api_response = APIResponse( - raw=response, - client=self, - cast_to=cast("type[ResponseT]", cast_to), # pyright: ignore[reportUnnecessaryCast] - stream=stream, - stream_cls=stream_cls, - options=options, - retries_taken=retries_taken, - ) - if bool(response.request.headers.get(RAW_RESPONSE_HEADER)): - return cast(ResponseT, api_response) - - return api_response.parse() - - def _request_api_list( - self, - model: Type[object], - page: Type[SyncPageT], - options: FinalRequestOptions, - ) -> SyncPageT: - def _parser(resp: SyncPageT) -> SyncPageT: - resp._set_private_attributes( - client=self, - model=model, - options=options, - ) - return resp - - options.post_parser = _parser - - return self.request(page, options, stream=False) - - @overload - def get( - self, - path: str, - *, - cast_to: Type[ResponseT], - options: RequestOptions = {}, - stream: Literal[False] = False, - ) -> ResponseT: ... - - @overload - def get( - self, - path: str, - *, - cast_to: Type[ResponseT], - options: RequestOptions = {}, - stream: Literal[True], - stream_cls: type[_StreamT], - ) -> _StreamT: ... - - @overload - def get( - self, - path: str, - *, - cast_to: Type[ResponseT], - options: RequestOptions = {}, - stream: bool, - stream_cls: type[_StreamT] | None = None, - ) -> ResponseT | _StreamT: ... - - def get( - self, - path: str, - *, - cast_to: Type[ResponseT], - options: RequestOptions = {}, - stream: bool = False, - stream_cls: type[_StreamT] | None = None, - ) -> ResponseT | _StreamT: - opts = FinalRequestOptions.construct(method="get", url=path, **options) - # cast is required because mypy complains about returning Any even though - # it understands the type variables - return cast(ResponseT, self.request(cast_to, opts, stream=stream, stream_cls=stream_cls)) - - @overload - def post( - self, - path: str, - *, - cast_to: Type[ResponseT], - body: Body | None = None, - options: RequestOptions = {}, - files: RequestFiles | None = None, - stream: Literal[False] = False, - ) -> ResponseT: ... - - @overload - def post( - self, - path: str, - *, - cast_to: Type[ResponseT], - body: Body | None = None, - options: RequestOptions = {}, - files: RequestFiles | None = None, - stream: Literal[True], - stream_cls: type[_StreamT], - ) -> _StreamT: ... - - @overload - def post( - self, - path: str, - *, - cast_to: Type[ResponseT], - body: Body | None = None, - options: RequestOptions = {}, - files: RequestFiles | None = None, - stream: bool, - stream_cls: type[_StreamT] | None = None, - ) -> ResponseT | _StreamT: ... - - def post( - self, - path: str, - *, - cast_to: Type[ResponseT], - body: Body | None = None, - options: RequestOptions = {}, - files: RequestFiles | None = None, - stream: bool = False, - stream_cls: type[_StreamT] | None = None, - ) -> ResponseT | _StreamT: - opts = FinalRequestOptions.construct( - method="post", url=path, json_data=body, files=to_httpx_files(files), **options - ) - return cast(ResponseT, self.request(cast_to, opts, stream=stream, stream_cls=stream_cls)) - - def patch( - self, - path: str, - *, - cast_to: Type[ResponseT], - body: Body | None = None, - options: RequestOptions = {}, - ) -> ResponseT: - opts = FinalRequestOptions.construct(method="patch", url=path, json_data=body, **options) - return self.request(cast_to, opts) - - def put( - self, - path: str, - *, - cast_to: Type[ResponseT], - body: Body | None = None, - files: RequestFiles | None = None, - options: RequestOptions = {}, - ) -> ResponseT: - opts = FinalRequestOptions.construct( - method="put", url=path, json_data=body, files=to_httpx_files(files), **options - ) - return self.request(cast_to, opts) - - def delete( - self, - path: str, - *, - cast_to: Type[ResponseT], - body: Body | None = None, - options: RequestOptions = {}, - ) -> ResponseT: - opts = FinalRequestOptions.construct(method="delete", url=path, json_data=body, **options) - return self.request(cast_to, opts) - - def get_api_list( - self, - path: str, - *, - model: Type[object], - page: Type[SyncPageT], - body: Body | None = None, - options: RequestOptions = {}, - method: str = "get", - ) -> SyncPageT: - opts = FinalRequestOptions.construct(method=method, url=path, json_data=body, **options) - return self._request_api_list(model, page, opts) - - -class _DefaultAsyncHttpxClient(httpx.AsyncClient): - def __init__(self, **kwargs: Any) -> None: - kwargs.setdefault("timeout", DEFAULT_TIMEOUT) - kwargs.setdefault("limits", DEFAULT_CONNECTION_LIMITS) - kwargs.setdefault("follow_redirects", True) - super().__init__(**kwargs) - - -try: - import httpx_aiohttp -except ImportError: - - class _DefaultAioHttpClient(httpx.AsyncClient): - def __init__(self, **_kwargs: Any) -> None: - raise RuntimeError("To use the aiohttp client you must have installed the package with the `aiohttp` extra") -else: - - class _DefaultAioHttpClient(httpx_aiohttp.HttpxAiohttpClient): # type: ignore - def __init__(self, **kwargs: Any) -> None: - kwargs.setdefault("timeout", DEFAULT_TIMEOUT) - kwargs.setdefault("limits", DEFAULT_CONNECTION_LIMITS) - kwargs.setdefault("follow_redirects", True) - - super().__init__(**kwargs) - - -if TYPE_CHECKING: - DefaultAsyncHttpxClient = httpx.AsyncClient - """An alias to `httpx.AsyncClient` that provides the same defaults that this SDK - uses internally. - - This is useful because overriding the `http_client` with your own instance of - `httpx.AsyncClient` will result in httpx's defaults being used, not ours. - """ - - DefaultAioHttpClient = httpx.AsyncClient - """An alias to `httpx.AsyncClient` that changes the default HTTP transport to `aiohttp`.""" -else: - DefaultAsyncHttpxClient = _DefaultAsyncHttpxClient - DefaultAioHttpClient = _DefaultAioHttpClient - - -class AsyncHttpxClientWrapper(DefaultAsyncHttpxClient): - def __del__(self) -> None: - if self.is_closed: - return - - try: - # TODO(someday): support non asyncio runtimes here - asyncio.get_running_loop().create_task(self.aclose()) - except Exception: - pass - - -class AsyncAPIClient(BaseClient[httpx.AsyncClient, AsyncStream[Any]]): - _client: httpx.AsyncClient - _default_stream_cls: type[AsyncStream[Any]] | None = None - - def __init__( - self, - *, - version: str, - base_url: str | URL, - _strict_response_validation: bool, - max_retries: int = DEFAULT_MAX_RETRIES, - timeout: float | Timeout | None | NotGiven = NOT_GIVEN, - http_client: httpx.AsyncClient | None = None, - custom_headers: Mapping[str, str] | None = None, - custom_query: Mapping[str, object] | None = None, - ) -> None: - if not is_given(timeout): - # if the user passed in a custom http client with a non-default - # timeout set then we use that timeout. - # - # note: there is an edge case here where the user passes in a client - # where they've explicitly set the timeout to match the default timeout - # as this check is structural, meaning that we'll think they didn't - # pass in a timeout and will ignore it - if http_client and http_client.timeout != HTTPX_DEFAULT_TIMEOUT: - timeout = http_client.timeout - else: - timeout = DEFAULT_TIMEOUT - - if http_client is not None and not isinstance(http_client, httpx.AsyncClient): # pyright: ignore[reportUnnecessaryIsInstance] - raise TypeError( - f"Invalid `http_client` argument; Expected an instance of `httpx.AsyncClient` but got {type(http_client)}" - ) - - super().__init__( - version=version, - base_url=base_url, - # cast to a valid type because mypy doesn't understand our type narrowing - timeout=cast(Timeout, timeout), - max_retries=max_retries, - custom_query=custom_query, - custom_headers=custom_headers, - _strict_response_validation=_strict_response_validation, - ) - self._client = http_client or AsyncHttpxClientWrapper( - base_url=base_url, - # cast to a valid type because mypy doesn't understand our type narrowing - timeout=cast(Timeout, timeout), - ) - - def is_closed(self) -> bool: - return self._client.is_closed - - async def close(self) -> None: - """Close the underlying HTTPX client. - - The client will *not* be usable after this. - """ - await self._client.aclose() - - async def __aenter__(self: _T) -> _T: - return self - - async def __aexit__( - self, - exc_type: type[BaseException] | None, - exc: BaseException | None, - exc_tb: TracebackType | None, - ) -> None: - await self.close() - - async def _prepare_options( - self, - options: FinalRequestOptions, # noqa: ARG002 - ) -> FinalRequestOptions: - """Hook for mutating the given options""" - return options - - async def _prepare_request( - self, - request: httpx.Request, # noqa: ARG002 - ) -> None: - """This method is used as a callback for mutating the `Request` object - after it has been constructed. - This is useful for cases where you want to add certain headers based off of - the request properties, e.g. `url`, `method` etc. - """ - return None - - @overload - async def request( - self, - cast_to: Type[ResponseT], - options: FinalRequestOptions, - *, - stream: Literal[False] = False, - ) -> ResponseT: ... - - @overload - async def request( - self, - cast_to: Type[ResponseT], - options: FinalRequestOptions, - *, - stream: Literal[True], - stream_cls: type[_AsyncStreamT], - ) -> _AsyncStreamT: ... - - @overload - async def request( - self, - cast_to: Type[ResponseT], - options: FinalRequestOptions, - *, - stream: bool, - stream_cls: type[_AsyncStreamT] | None = None, - ) -> ResponseT | _AsyncStreamT: ... - - async def request( - self, - cast_to: Type[ResponseT], - options: FinalRequestOptions, - *, - stream: bool = False, - stream_cls: type[_AsyncStreamT] | None = None, - ) -> ResponseT | _AsyncStreamT: - if self._platform is None: - # `get_platform` can make blocking IO calls so we - # execute it earlier while we are in an async context - self._platform = await asyncify(get_platform)() - - cast_to = self._maybe_override_cast_to(cast_to, options) - - # create a copy of the options we were given so that if the - # options are mutated later & we then retry, the retries are - # given the original options - input_options = model_copy(options) - if input_options.idempotency_key is None and input_options.method.lower() != "get": - # ensure the idempotency key is reused between requests - input_options.idempotency_key = self._idempotency_key() - - response: httpx.Response | None = None - max_retries = input_options.get_max_retries(self.max_retries) - - retries_taken = 0 - for retries_taken in range(max_retries + 1): - options = model_copy(input_options) - options = await self._prepare_options(options) - - remaining_retries = max_retries - retries_taken - request = self._build_request(options, retries_taken=retries_taken) - await self._prepare_request(request) - - kwargs: HttpxSendArgs = {} - if self.custom_auth is not None: - kwargs["auth"] = self.custom_auth - - if options.follow_redirects is not None: - kwargs["follow_redirects"] = options.follow_redirects - - log.debug("Sending HTTP Request: %s %s", request.method, request.url) - - response = None - try: - response = await self._client.send( - request, - stream=stream or self._should_stream_response_body(request=request), - **kwargs, - ) - except httpx.TimeoutException as err: - log.debug("Encountered httpx.TimeoutException", exc_info=True) - - if remaining_retries > 0: - await self._sleep_for_retry( - retries_taken=retries_taken, - max_retries=max_retries, - options=input_options, - response=None, - ) - continue - - log.debug("Raising timeout error") - raise APITimeoutError(request=request) from err - except Exception as err: - log.debug("Encountered Exception", exc_info=True) - - if remaining_retries > 0: - await self._sleep_for_retry( - retries_taken=retries_taken, - max_retries=max_retries, - options=input_options, - response=None, - ) - continue - - log.debug("Raising connection error") - raise APIConnectionError(request=request) from err - - log.debug( - 'HTTP Response: %s %s "%i %s" %s', - request.method, - request.url, - response.status_code, - response.reason_phrase, - response.headers, - ) - - try: - response.raise_for_status() - except httpx.HTTPStatusError as err: # thrown on 4xx and 5xx status code - log.debug("Encountered httpx.HTTPStatusError", exc_info=True) - - if remaining_retries > 0 and self._should_retry(err.response): - await err.response.aclose() - await self._sleep_for_retry( - retries_taken=retries_taken, - max_retries=max_retries, - options=input_options, - response=response, - ) - continue - - # If the response is streamed then we need to explicitly read the response - # to completion before attempting to access the response text. - if not err.response.is_closed: - await err.response.aread() - - log.debug("Re-raising status error") - raise self._make_status_error_from_response(err.response) from None - - break - - assert response is not None, "could not resolve response (should never happen)" - return await self._process_response( - cast_to=cast_to, - options=options, - response=response, - stream=stream, - stream_cls=stream_cls, - retries_taken=retries_taken, - ) - - async def _sleep_for_retry( - self, *, retries_taken: int, max_retries: int, options: FinalRequestOptions, response: httpx.Response | None - ) -> None: - remaining_retries = max_retries - retries_taken - if remaining_retries == 1: - log.debug("1 retry left") - else: - log.debug("%i retries left", remaining_retries) - - timeout = self._calculate_retry_timeout(remaining_retries, options, response.headers if response else None) - log.info("Retrying request to %s in %f seconds", options.url, timeout) - - await anyio.sleep(timeout) - - async def _process_response( - self, - *, - cast_to: Type[ResponseT], - options: FinalRequestOptions, - response: httpx.Response, - stream: bool, - stream_cls: type[Stream[Any]] | type[AsyncStream[Any]] | None, - retries_taken: int = 0, - ) -> ResponseT: - origin = get_origin(cast_to) or cast_to - - if ( - inspect.isclass(origin) - and issubclass(origin, BaseAPIResponse) - # we only want to actually return the custom BaseAPIResponse class if we're - # returning the raw response, or if we're not streaming SSE, as if we're streaming - # SSE then `cast_to` doesn't actively reflect the type we need to parse into - and (not stream or bool(response.request.headers.get(RAW_RESPONSE_HEADER))) - ): - if not issubclass(origin, AsyncAPIResponse): - raise TypeError(f"API Response types must subclass {AsyncAPIResponse}; Received {origin}") - - response_cls = cast("type[BaseAPIResponse[Any]]", cast_to) - return cast( - "ResponseT", - response_cls( - raw=response, - client=self, - cast_to=extract_response_type(response_cls), - stream=stream, - stream_cls=stream_cls, - options=options, - retries_taken=retries_taken, - ), - ) - - if cast_to == httpx.Response: - return cast(ResponseT, response) - - api_response = AsyncAPIResponse( - raw=response, - client=self, - cast_to=cast("type[ResponseT]", cast_to), # pyright: ignore[reportUnnecessaryCast] - stream=stream, - stream_cls=stream_cls, - options=options, - retries_taken=retries_taken, - ) - if bool(response.request.headers.get(RAW_RESPONSE_HEADER)): - return cast(ResponseT, api_response) - - return await api_response.parse() - - def _request_api_list( - self, - model: Type[_T], - page: Type[AsyncPageT], - options: FinalRequestOptions, - ) -> AsyncPaginator[_T, AsyncPageT]: - return AsyncPaginator(client=self, options=options, page_cls=page, model=model) - - @overload - async def get( - self, - path: str, - *, - cast_to: Type[ResponseT], - options: RequestOptions = {}, - stream: Literal[False] = False, - ) -> ResponseT: ... - - @overload - async def get( - self, - path: str, - *, - cast_to: Type[ResponseT], - options: RequestOptions = {}, - stream: Literal[True], - stream_cls: type[_AsyncStreamT], - ) -> _AsyncStreamT: ... - - @overload - async def get( - self, - path: str, - *, - cast_to: Type[ResponseT], - options: RequestOptions = {}, - stream: bool, - stream_cls: type[_AsyncStreamT] | None = None, - ) -> ResponseT | _AsyncStreamT: ... - - async def get( - self, - path: str, - *, - cast_to: Type[ResponseT], - options: RequestOptions = {}, - stream: bool = False, - stream_cls: type[_AsyncStreamT] | None = None, - ) -> ResponseT | _AsyncStreamT: - opts = FinalRequestOptions.construct(method="get", url=path, **options) - return await self.request(cast_to, opts, stream=stream, stream_cls=stream_cls) - - @overload - async def post( - self, - path: str, - *, - cast_to: Type[ResponseT], - body: Body | None = None, - files: RequestFiles | None = None, - options: RequestOptions = {}, - stream: Literal[False] = False, - ) -> ResponseT: ... - - @overload - async def post( - self, - path: str, - *, - cast_to: Type[ResponseT], - body: Body | None = None, - files: RequestFiles | None = None, - options: RequestOptions = {}, - stream: Literal[True], - stream_cls: type[_AsyncStreamT], - ) -> _AsyncStreamT: ... - - @overload - async def post( - self, - path: str, - *, - cast_to: Type[ResponseT], - body: Body | None = None, - files: RequestFiles | None = None, - options: RequestOptions = {}, - stream: bool, - stream_cls: type[_AsyncStreamT] | None = None, - ) -> ResponseT | _AsyncStreamT: ... - - async def post( - self, - path: str, - *, - cast_to: Type[ResponseT], - body: Body | None = None, - files: RequestFiles | None = None, - options: RequestOptions = {}, - stream: bool = False, - stream_cls: type[_AsyncStreamT] | None = None, - ) -> ResponseT | _AsyncStreamT: - opts = FinalRequestOptions.construct( - method="post", url=path, json_data=body, files=await async_to_httpx_files(files), **options - ) - return await self.request(cast_to, opts, stream=stream, stream_cls=stream_cls) - - async def patch( - self, - path: str, - *, - cast_to: Type[ResponseT], - body: Body | None = None, - options: RequestOptions = {}, - ) -> ResponseT: - opts = FinalRequestOptions.construct(method="patch", url=path, json_data=body, **options) - return await self.request(cast_to, opts) - - async def put( - self, - path: str, - *, - cast_to: Type[ResponseT], - body: Body | None = None, - files: RequestFiles | None = None, - options: RequestOptions = {}, - ) -> ResponseT: - opts = FinalRequestOptions.construct( - method="put", url=path, json_data=body, files=await async_to_httpx_files(files), **options - ) - return await self.request(cast_to, opts) - - async def delete( - self, - path: str, - *, - cast_to: Type[ResponseT], - body: Body | None = None, - options: RequestOptions = {}, - ) -> ResponseT: - opts = FinalRequestOptions.construct(method="delete", url=path, json_data=body, **options) - return await self.request(cast_to, opts) - - def get_api_list( - self, - path: str, - *, - model: Type[_T], - page: Type[AsyncPageT], - body: Body | None = None, - options: RequestOptions = {}, - method: str = "get", - ) -> AsyncPaginator[_T, AsyncPageT]: - opts = FinalRequestOptions.construct(method=method, url=path, json_data=body, **options) - return self._request_api_list(model, page, opts) - - -def make_request_options( - *, - query: Query | None = None, - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - idempotency_key: str | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - post_parser: PostParser | NotGiven = NOT_GIVEN, -) -> RequestOptions: - """Create a dict of type RequestOptions without keys of NotGiven values.""" - options: RequestOptions = {} - if extra_headers is not None: - options["headers"] = extra_headers - - if extra_body is not None: - options["extra_json"] = cast(AnyMapping, extra_body) - - if query is not None: - options["params"] = query - - if extra_query is not None: - options["params"] = {**options.get("params", {}), **extra_query} - - if not isinstance(timeout, NotGiven): - options["timeout"] = timeout - - if idempotency_key is not None: - options["idempotency_key"] = idempotency_key - - if is_given(post_parser): - # internal - options["post_parser"] = post_parser # type: ignore - - return options - - -class ForceMultipartDict(Dict[str, None]): - def __bool__(self) -> bool: - return True - - -class OtherPlatform: - def __init__(self, name: str) -> None: - self.name = name - - @override - def __str__(self) -> str: - return f"Other:{self.name}" - - -Platform = Union[ - OtherPlatform, - Literal[ - "MacOS", - "Linux", - "Windows", - "FreeBSD", - "OpenBSD", - "iOS", - "Android", - "Unknown", - ], -] - - -def get_platform() -> Platform: - try: - system = platform.system().lower() - platform_name = platform.platform().lower() - except Exception: - return "Unknown" - - if "iphone" in platform_name or "ipad" in platform_name: - # Tested using Python3IDE on an iPhone 11 and Pythonista on an iPad 7 - # system is Darwin and platform_name is a string like: - # - Darwin-21.6.0-iPhone12,1-64bit - # - Darwin-21.6.0-iPad7,11-64bit - return "iOS" - - if system == "darwin": - return "MacOS" - - if system == "windows": - return "Windows" - - if "android" in platform_name: - # Tested using Pydroid 3 - # system is Linux and platform_name is a string like 'Linux-5.10.81-android12-9-00001-geba40aecb3b7-ab8534902-aarch64-with-libc' - return "Android" - - if system == "linux": - # https://distro.readthedocs.io/en/latest/#distro.id - distro_id = distro.id() - if distro_id == "freebsd": - return "FreeBSD" - - if distro_id == "openbsd": - return "OpenBSD" - - return "Linux" - - if platform_name: - return OtherPlatform(platform_name) - - return "Unknown" - - -@lru_cache(maxsize=None) -def platform_headers(version: str, *, platform: Platform | None) -> Dict[str, str]: - return { - "X-Stainless-Lang": "python", - "X-Stainless-Package-Version": version, - "X-Stainless-OS": str(platform or get_platform()), - "X-Stainless-Arch": str(get_architecture()), - "X-Stainless-Runtime": get_python_runtime(), - "X-Stainless-Runtime-Version": get_python_version(), - } - - -class OtherArch: - def __init__(self, name: str) -> None: - self.name = name - - @override - def __str__(self) -> str: - return f"other:{self.name}" - - -Arch = Union[OtherArch, Literal["x32", "x64", "arm", "arm64", "unknown"]] - - -def get_python_runtime() -> str: - try: - return platform.python_implementation() - except Exception: - return "unknown" - - -def get_python_version() -> str: - try: - return platform.python_version() - except Exception: - return "unknown" - - -def get_architecture() -> Arch: - try: - machine = platform.machine().lower() - except Exception: - return "unknown" - - if machine in ("arm64", "aarch64"): - return "arm64" - - # TODO: untested - if machine == "arm": - return "arm" - - if machine == "x86_64": - return "x64" - - # TODO: untested - if sys.maxsize <= 2**32: - return "x32" - - if machine: - return OtherArch(machine) - - return "unknown" - - -def _merge_mappings( - obj1: Mapping[_T_co, Union[_T, Omit]], - obj2: Mapping[_T_co, Union[_T, Omit]], -) -> Dict[_T_co, _T]: - """Merge two mappings of the same type, removing any values that are instances of `Omit`. - - In cases with duplicate keys the second mapping takes precedence. - """ - merged = {**obj1, **obj2} - return {key: value for key, value in merged.items() if not isinstance(value, Omit)} diff --git a/src/browser_use_sdk/_client.py b/src/browser_use_sdk/_client.py deleted file mode 100644 index 13ad53e..0000000 --- a/src/browser_use_sdk/_client.py +++ /dev/null @@ -1,439 +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, Union, Mapping -from typing_extensions import Self, override - -import httpx - -from . import _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 .resources import tasks, agent_profiles, browser_profiles -from ._streaming import Stream as Stream, AsyncStream as AsyncStream -from ._exceptions import APIStatusError, BrowserUseError -from ._base_client import ( - DEFAULT_MAX_RETRIES, - SyncAPIClient, - AsyncAPIClient, -) -from .resources.users import users -from .resources.sessions import sessions - -__all__ = [ - "Timeout", - "Transport", - "ProxiesTypes", - "RequestOptions", - "BrowserUse", - "AsyncBrowserUse", - "Client", - "AsyncClient", -] - - -class BrowserUse(SyncAPIClient): - users: users.UsersResource - tasks: tasks.TasksResource - sessions: sessions.SessionsResource - browser_profiles: browser_profiles.BrowserProfilesResource - agent_profiles: agent_profiles.AgentProfilesResource - with_raw_response: BrowserUseWithRawResponse - with_streaming_response: BrowserUseWithStreamedResponse - - # 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 BrowserUse client instance. - - This automatically infers the `api_key` argument from the `BROWSER_USE_API_KEY` environment variable if it is not provided. - """ - if api_key is None: - api_key = os.environ.get("BROWSER_USE_API_KEY") - if api_key is None: - raise BrowserUseError( - "The api_key client option must be set either by passing api_key to the client or by setting the BROWSER_USE_API_KEY environment variable" - ) - self.api_key = api_key - - if base_url is None: - base_url = os.environ.get("BROWSER_USE_BASE_URL") - if base_url is None: - base_url = f"https://api.browser-use.com/api/v2" - - 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.users = users.UsersResource(self) - self.tasks = tasks.TasksResource(self) - self.sessions = sessions.SessionsResource(self) - self.browser_profiles = browser_profiles.BrowserProfilesResource(self) - self.agent_profiles = agent_profiles.AgentProfilesResource(self) - self.with_raw_response = BrowserUseWithRawResponse(self) - self.with_streaming_response = BrowserUseWithStreamedResponse(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-Browser-Use-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 AsyncBrowserUse(AsyncAPIClient): - users: users.AsyncUsersResource - tasks: tasks.AsyncTasksResource - sessions: sessions.AsyncSessionsResource - browser_profiles: browser_profiles.AsyncBrowserProfilesResource - agent_profiles: agent_profiles.AsyncAgentProfilesResource - with_raw_response: AsyncBrowserUseWithRawResponse - with_streaming_response: AsyncBrowserUseWithStreamedResponse - - # 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 AsyncBrowserUse client instance. - - This automatically infers the `api_key` argument from the `BROWSER_USE_API_KEY` environment variable if it is not provided. - """ - if api_key is None: - api_key = os.environ.get("BROWSER_USE_API_KEY") - if api_key is None: - raise BrowserUseError( - "The api_key client option must be set either by passing api_key to the client or by setting the BROWSER_USE_API_KEY environment variable" - ) - self.api_key = api_key - - if base_url is None: - base_url = os.environ.get("BROWSER_USE_BASE_URL") - if base_url is None: - base_url = f"https://api.browser-use.com/api/v2" - - 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.users = users.AsyncUsersResource(self) - self.tasks = tasks.AsyncTasksResource(self) - self.sessions = sessions.AsyncSessionsResource(self) - self.browser_profiles = browser_profiles.AsyncBrowserProfilesResource(self) - self.agent_profiles = agent_profiles.AsyncAgentProfilesResource(self) - self.with_raw_response = AsyncBrowserUseWithRawResponse(self) - self.with_streaming_response = AsyncBrowserUseWithStreamedResponse(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-Browser-Use-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 BrowserUseWithRawResponse: - def __init__(self, client: BrowserUse) -> None: - self.users = users.UsersResourceWithRawResponse(client.users) - self.tasks = tasks.TasksResourceWithRawResponse(client.tasks) - self.sessions = sessions.SessionsResourceWithRawResponse(client.sessions) - self.browser_profiles = browser_profiles.BrowserProfilesResourceWithRawResponse(client.browser_profiles) - self.agent_profiles = agent_profiles.AgentProfilesResourceWithRawResponse(client.agent_profiles) - - -class AsyncBrowserUseWithRawResponse: - def __init__(self, client: AsyncBrowserUse) -> None: - self.users = users.AsyncUsersResourceWithRawResponse(client.users) - self.tasks = tasks.AsyncTasksResourceWithRawResponse(client.tasks) - self.sessions = sessions.AsyncSessionsResourceWithRawResponse(client.sessions) - self.browser_profiles = browser_profiles.AsyncBrowserProfilesResourceWithRawResponse(client.browser_profiles) - self.agent_profiles = agent_profiles.AsyncAgentProfilesResourceWithRawResponse(client.agent_profiles) - - -class BrowserUseWithStreamedResponse: - def __init__(self, client: BrowserUse) -> None: - self.users = users.UsersResourceWithStreamingResponse(client.users) - self.tasks = tasks.TasksResourceWithStreamingResponse(client.tasks) - self.sessions = sessions.SessionsResourceWithStreamingResponse(client.sessions) - self.browser_profiles = browser_profiles.BrowserProfilesResourceWithStreamingResponse(client.browser_profiles) - self.agent_profiles = agent_profiles.AgentProfilesResourceWithStreamingResponse(client.agent_profiles) - - -class AsyncBrowserUseWithStreamedResponse: - def __init__(self, client: AsyncBrowserUse) -> None: - self.users = users.AsyncUsersResourceWithStreamingResponse(client.users) - self.tasks = tasks.AsyncTasksResourceWithStreamingResponse(client.tasks) - self.sessions = sessions.AsyncSessionsResourceWithStreamingResponse(client.sessions) - self.browser_profiles = browser_profiles.AsyncBrowserProfilesResourceWithStreamingResponse( - client.browser_profiles - ) - self.agent_profiles = agent_profiles.AsyncAgentProfilesResourceWithStreamingResponse(client.agent_profiles) - - -Client = BrowserUse - -AsyncClient = AsyncBrowserUse diff --git a/src/browser_use_sdk/_compat.py b/src/browser_use_sdk/_compat.py deleted file mode 100644 index 92d9ee6..0000000 --- a/src/browser_use_sdk/_compat.py +++ /dev/null @@ -1,219 +0,0 @@ -from __future__ import annotations - -from typing import TYPE_CHECKING, Any, Union, Generic, TypeVar, Callable, cast, overload -from datetime import date, datetime -from typing_extensions import Self, Literal - -import pydantic -from pydantic.fields import FieldInfo - -from ._types import IncEx, StrBytesIntFloat - -_T = TypeVar("_T") -_ModelT = TypeVar("_ModelT", bound=pydantic.BaseModel) - -# --------------- Pydantic v2 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 = None, - exclude_unset: bool = False, - exclude_defaults: bool = False, - warnings: bool = True, - mode: Literal["json", "python"] = "python", -) -> dict[str, Any]: - if PYDANTIC_V2 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, - ) - 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: - from functools import cached_property as cached_property - - typed_cached_property = cached_property diff --git a/src/browser_use_sdk/_constants.py b/src/browser_use_sdk/_constants.py deleted file mode 100644 index 6ddf2c7..0000000 --- a/src/browser_use_sdk/_constants.py +++ /dev/null @@ -1,14 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -import httpx - -RAW_RESPONSE_HEADER = "X-Stainless-Raw-Response" -OVERRIDE_CAST_TO_HEADER = "____stainless_override_cast_to" - -# default timeout is 1 minute -DEFAULT_TIMEOUT = httpx.Timeout(timeout=60, connect=5.0) -DEFAULT_MAX_RETRIES = 2 -DEFAULT_CONNECTION_LIMITS = httpx.Limits(max_connections=100, max_keepalive_connections=20) - -INITIAL_RETRY_DELAY = 0.5 -MAX_RETRY_DELAY = 8.0 diff --git a/src/browser_use_sdk/_exceptions.py b/src/browser_use_sdk/_exceptions.py deleted file mode 100644 index 125f8b3..0000000 --- a/src/browser_use_sdk/_exceptions.py +++ /dev/null @@ -1,108 +0,0 @@ -# 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 BrowserUseError(Exception): - pass - - -class APIError(BrowserUseError): - 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/browser_use_sdk/_files.py b/src/browser_use_sdk/_files.py deleted file mode 100644 index cc14c14..0000000 --- a/src/browser_use_sdk/_files.py +++ /dev/null @@ -1,123 +0,0 @@ -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/browser_use_sdk/_models.py b/src/browser_use_sdk/_models.py deleted file mode 100644 index b8387ce..0000000 --- a/src/browser_use_sdk/_models.py +++ /dev/null @@ -1,829 +0,0 @@ -from __future__ import annotations - -import os -import inspect -from typing import TYPE_CHECKING, Any, Type, Union, Generic, TypeVar, Callable, Optional, cast -from datetime import date, datetime -from typing_extensions import ( - List, - Unpack, - Literal, - ClassVar, - Protocol, - Required, - ParamSpec, - TypedDict, - TypeGuard, - final, - override, - runtime_checkable, -) - -import pydantic -from pydantic.fields import FieldInfo - -from ._types import ( - Body, - IncEx, - Query, - ModelT, - Headers, - Timeout, - NotGiven, - AnyMapping, - HttpxRequestFiles, -) -from ._utils import ( - PropertyInfo, - is_list, - is_given, - json_safe, - lru_cache, - is_mapping, - parse_date, - coerce_boolean, - parse_datetime, - strip_not_given, - extract_type_arg, - is_annotated_type, - is_type_alias_type, - strip_annotated_type, -) -from ._compat import ( - PYDANTIC_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, ModelSchema, LiteralSchema, ModelFieldsSchema - -__all__ = ["BaseModel", "GenericModel"] - -_T = TypeVar("_T") -_BaseModelT = TypeVar("_BaseModelT", bound="BaseModel") - -P = ParamSpec("P") - - -@runtime_checkable -class _ConfigProtocol(Protocol): - allow_population_by_field_name: bool - - -class BaseModel(pydantic.BaseModel): - if PYDANTIC_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( # pyright: ignore[reportIncompatibleMethodOverride] - __cls: Type[ModelT], - _fields_set: set[str] | None = None, - **values: object, - ) -> ModelT: - m = __cls.__new__(__cls) - fields_values: dict[str, object] = {} - - config = get_model_config(__cls) - populate_by_name = ( - config.allow_population_by_field_name - if isinstance(config, _ConfigProtocol) - else config.get("populate_by_name") - ) - - if _fields_set is None: - _fields_set = set() - - model_fields = get_model_fields(__cls) - for name, field in model_fields.items(): - key = field.alias - if key is None or (key not in values and populate_by_name): - key = name - - if key in values: - fields_values[name] = _construct_field(value=values[key], field=field, key=key) - _fields_set.add(name) - else: - fields_values[name] = field_get_default(field) - - extra_field_type = _get_extra_fields_type(__cls) - - _extra = {} - for key, value in values.items(): - if key not in model_fields: - parsed = construct_type(value=value, type_=extra_field_type) if extra_field_type is not None else value - - if PYDANTIC_V2: - _extra[key] = parsed - else: - _fields_set.add(key) - fields_values[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: - # 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 = None, - exclude: IncEx | None = 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 not in {"json", "python"}: - raise ValueError("mode must be either 'json' or 'python'") - if round_trip != False: - raise ValueError("round_trip is only supported in Pydantic v2") - if warnings != True: - raise ValueError("warnings is only supported in Pydantic v2") - if context is not None: - raise ValueError("context is only supported in Pydantic v2") - if serialize_as_any != False: - raise ValueError("serialize_as_any is only supported in Pydantic v2") - dumped = 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, - ) - - return cast(dict[str, Any], json_safe(dumped)) if mode == "json" else dumped - - @override - def model_dump_json( - self, - *, - indent: int | None = None, - include: IncEx | None = None, - exclude: IncEx | None = 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_, 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_): - for variant in get_args(type_): - if is_basemodel(variant): - return True - - return False - - return is_basemodel_type(type_) - - -def is_basemodel_type(type_: type) -> TypeGuard[type[BaseModel] | type[GenericModel]]: - origin = get_origin(type_) or type_ - if not inspect.isclass(origin): - return False - return issubclass(origin, BaseModel) or issubclass(origin, GenericModel) - - -def build( - base_model_cls: Callable[P, _BaseModelT], - *args: P.args, - **kwargs: P.kwargs, -) -> _BaseModelT: - """Construct a BaseModel class without validation. - - This is useful for cases where you need to instantiate a `BaseModel` - from an API response as this provides type-safe params which isn't supported - by helpers like `construct_type()`. - - ```py - build(MyModel, my_field_a="foo", my_field_b=123) - ``` - """ - if args: - raise TypeError( - "Received positional arguments which are not supported; Keyword arguments must be used instead", - ) - - return cast(_BaseModelT, construct_type(type_=base_model_cls, value=kwargs)) - - -def construct_type_unchecked(*, value: object, type_: type[_T]) -> _T: - """Loose coercion to the expected type with construction of nested values. - - Note: the returned value from this function is not guaranteed to match the - given type. - """ - return cast(_T, construct_type(value=value, type_=type_)) - - -def construct_type(*, value: object, type_: object, metadata: Optional[List[Any]] = None) -> object: - """Loose coercion to the expected type with construction of nested values. - - If the given value does not match the expected type then it is returned as-is. - """ - - # store a reference to the original type we were given before we extract any inner - # types so that we can properly resolve forward references in `TypeAliasType` annotations - original_type = None - - # we allow `object` as the input type because otherwise, passing things like - # `Literal['value']` will be reported as a type error by type checkers - type_ = cast("type[object]", type_) - if is_type_alias_type(type_): - original_type = type_ # type: ignore[unreachable] - type_ = type_.__value__ # type: ignore[unreachable] - - # unwrap `Annotated[T, ...]` -> `T` - if metadata is not None and len(metadata) > 0: - meta: tuple[Any, ...] = tuple(metadata) - elif is_annotated_type(type_): - meta = get_args(type_)[1:] - type_ = extract_type_arg(type_, 0) - else: - meta = tuple() - - # we need to use the origin class for any types that are subscripted generics - # e.g. Dict[str, object] - origin = get_origin(type_) or type_ - args = get_args(type_) - - if is_union(origin): - try: - return validate_type(type_=cast("type[object]", original_type or type_), value=value) - except Exception: - pass - - # if the type is a discriminated union then we want to construct the right variant - # in the union, even if the data doesn't match exactly, otherwise we'd break code - # that relies on the constructed class types, e.g. - # - # class FooType: - # kind: Literal['foo'] - # value: str - # - # class BarType: - # kind: Literal['bar'] - # value: int - # - # without this block, if the data we get is something like `{'kind': 'bar', 'value': 'foo'}` then - # we'd end up constructing `FooType` when it should be `BarType`. - discriminator = _build_discriminated_union_meta(union=type_, meta_annotations=meta) - if discriminator and is_mapping(value): - variant_value = value.get(discriminator.field_alias_from or discriminator.field_name) - if variant_value and isinstance(variant_value, str): - variant_type = discriminator.mapping.get(variant_value) - if variant_type: - return construct_type(type_=variant_type, value=value) - - # if the data is not valid, use the first variant that doesn't fail while deserializing - for variant in args: - try: - return construct_type(value=value, type_=variant) - except Exception: - continue - - raise RuntimeError(f"Could not convert data into a valid instance of {type_}") - - if origin == dict: - if not is_mapping(value): - return value - - _, items_type = get_args(type_) # Dict[_, items_type] - return {key: construct_type(value=item, type_=items_type) for key, item in value.items()} - - if ( - not is_literal_type(type_) - and inspect.isclass(origin) - and (issubclass(origin, BaseModel) or issubclass(origin, GenericModel)) - ): - if is_list(value): - return [cast(Any, type_).construct(**entry) if is_mapping(entry) else entry for entry in value] - - if is_mapping(value): - if issubclass(type_, BaseModel): - return type_.construct(**value) # type: ignore[arg-type] - - return cast(Any, type_).construct(**value) - - if origin == list: - if not is_list(value): - return value - - inner_type = args[0] # List[inner_type] - return [construct_type(value=entry, type_=inner_type) for entry in value] - - if origin == float: - if isinstance(value, int): - coerced = float(value) - if coerced != value: - return value - return coerced - - return value - - if type_ == datetime: - try: - return parse_datetime(value) # type: ignore - except Exception: - return value - - if type_ == date: - try: - return parse_date(value) # type: ignore - except Exception: - return value - - return value - - -@runtime_checkable -class CachedDiscriminatorType(Protocol): - __discriminator__: DiscriminatorDetails - - -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 (annotation := getattr(field_info, "annotation", None)) and is_literal_type(annotation): - for entry in get_args(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"] == "definitions": - schema = schema["schema"] - - if schema["type"] != "model": - return None - - schema = cast("ModelSchema", schema) - fields_schema = schema["schema"] - if fields_schema["type"] != "model-fields": - return None - - fields_schema = cast("ModelFieldsSchema", fields_schema) - field = fields_schema["fields"].get(field_name) - if not field: - return None - - return cast("ModelField", field) # pyright: ignore[reportUnnecessaryCast] - - -def validate_type(*, type_: type[_T], value: object) -> _T: - """Strict validation that the given value matches the expected type""" - if inspect.isclass(type_) and issubclass(type_, pydantic.BaseModel): - return cast(_T, parse_obj(type_, value)) - - return cast(_T, _validate_non_model_type(type_=type_, value=value)) - - -def set_pydantic_config(typ: Any, config: pydantic.ConfigDict) -> None: - """Add a pydantic config for the given type. - - Note: this is a no-op on Pydantic v1. - """ - setattr(typ, "__pydantic_config__", config) # noqa: B010 - - -# our use of subclassing here causes weirdness for type checkers, -# so we just pretend that we don't subclass -if TYPE_CHECKING: - GenericModel = BaseModel -else: - - class GenericModel(BaseGenericModel, BaseModel): - pass - - -if 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 - follow_redirects: bool - - -@final -class FinalRequestOptions(pydantic.BaseModel): - method: str - url: str - params: Query = {} - headers: Union[Headers, NotGiven] = NotGiven() - max_retries: Union[int, NotGiven] = NotGiven() - timeout: Union[float, Timeout, None, NotGiven] = NotGiven() - files: Union[HttpxRequestFiles, None] = None - idempotency_key: Union[str, None] = None - post_parser: Union[Callable[[Any], Any], NotGiven] = NotGiven() - follow_redirects: Union[bool, None] = None - - # It should be noted that we cannot use `json` here as that would override - # a BaseModel method in an incompatible fashion. - json_data: Union[Body, None] = None - extra_json: Union[AnyMapping, None] = None - - if PYDANTIC_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/browser_use_sdk/_qs.py b/src/browser_use_sdk/_qs.py deleted file mode 100644 index 274320c..0000000 --- a/src/browser_use_sdk/_qs.py +++ /dev/null @@ -1,150 +0,0 @@ -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/browser_use_sdk/_resource.py b/src/browser_use_sdk/_resource.py deleted file mode 100644 index 770c41a..0000000 --- a/src/browser_use_sdk/_resource.py +++ /dev/null @@ -1,43 +0,0 @@ -# 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 BrowserUse, AsyncBrowserUse - - -class SyncAPIResource: - _client: BrowserUse - - def __init__(self, client: BrowserUse) -> 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: AsyncBrowserUse - - def __init__(self, client: AsyncBrowserUse) -> 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/browser_use_sdk/_response.py b/src/browser_use_sdk/_response.py deleted file mode 100644 index 28a84a0..0000000 --- a/src/browser_use_sdk/_response.py +++ /dev/null @@ -1,832 +0,0 @@ -from __future__ import annotations - -import os -import inspect -import logging -import datetime -import functools -from types import TracebackType -from typing import ( - TYPE_CHECKING, - Any, - Union, - Generic, - TypeVar, - Callable, - Iterator, - AsyncIterator, - cast, - overload, -) -from typing_extensions import Awaitable, ParamSpec, override, get_origin - -import anyio -import httpx -import pydantic - -from ._types import NoneType -from ._utils import is_given, extract_type_arg, is_annotated_type, is_type_alias_type, extract_type_var_from_base -from ._models import BaseModel, is_basemodel -from ._constants import RAW_RESPONSE_HEADER, OVERRIDE_CAST_TO_HEADER -from ._streaming import Stream, AsyncStream, is_stream_class_type, extract_stream_chunk_type -from ._exceptions import BrowserUseError, APIResponseValidationError - -if TYPE_CHECKING: - from ._models import FinalRequestOptions - from ._base_client import BaseClient - - -P = ParamSpec("P") -R = TypeVar("R") -_T = TypeVar("_T") -_APIResponseT = TypeVar("_APIResponseT", bound="APIResponse[Any]") -_AsyncAPIResponseT = TypeVar("_AsyncAPIResponseT", bound="AsyncAPIResponse[Any]") - -log: logging.Logger = logging.getLogger(__name__) - - -class BaseAPIResponse(Generic[R]): - _cast_to: type[R] - _client: BaseClient[Any, Any] - _parsed_by_type: dict[type[Any], Any] - _is_sse_stream: bool - _stream_cls: type[Stream[Any]] | type[AsyncStream[Any]] | None - _options: FinalRequestOptions - - http_response: httpx.Response - - retries_taken: int - """The number of retries made. If no retries happened this will be `0`""" - - def __init__( - self, - *, - raw: httpx.Response, - cast_to: type[R], - client: BaseClient[Any, Any], - stream: bool, - stream_cls: type[Stream[Any]] | type[AsyncStream[Any]] | None, - options: FinalRequestOptions, - retries_taken: int = 0, - ) -> None: - self._cast_to = cast_to - self._client = client - self._parsed_by_type = {} - self._is_sse_stream = stream - self._stream_cls = stream_cls - self._options = options - self.http_response = raw - self.retries_taken = retries_taken - - @property - def headers(self) -> httpx.Headers: - return self.http_response.headers - - @property - def http_request(self) -> httpx.Request: - """Returns the httpx Request instance associated with the current response.""" - return self.http_response.request - - @property - def status_code(self) -> int: - return self.http_response.status_code - - @property - def url(self) -> httpx.URL: - """Returns the URL for which the request was made.""" - return self.http_response.url - - @property - def method(self) -> str: - return self.http_request.method - - @property - def http_version(self) -> str: - return self.http_response.http_version - - @property - def elapsed(self) -> datetime.timedelta: - """The time taken for the complete request/response cycle to complete.""" - return self.http_response.elapsed - - @property - def is_closed(self) -> bool: - """Whether or not the response body has been closed. - - If this is False then there is response data that has not been read yet. - You must either fully consume the response body or call `.close()` - before discarding the response to prevent resource leaks. - """ - return self.http_response.is_closed - - @override - def __repr__(self) -> str: - return ( - f"<{self.__class__.__name__} [{self.status_code} {self.http_response.reason_phrase}] type={self._cast_to}>" - ) - - def _parse(self, *, to: type[_T] | None = None) -> R | _T: - cast_to = to if to is not None else self._cast_to - - # unwrap `TypeAlias('Name', T)` -> `T` - if is_type_alias_type(cast_to): - cast_to = cast_to.__value__ # type: ignore[unreachable] - - # unwrap `Annotated[T, ...]` -> `T` - if cast_to and is_annotated_type(cast_to): - cast_to = extract_type_arg(cast_to, 0) - - origin = get_origin(cast_to) or cast_to - - if self._is_sse_stream: - if to: - if not is_stream_class_type(to): - raise TypeError(f"Expected custom parse type to be a subclass of {Stream} or {AsyncStream}") - - return cast( - _T, - to( - cast_to=extract_stream_chunk_type( - to, - failure_message="Expected custom stream type to be passed with a type argument, e.g. Stream[ChunkType]", - ), - response=self.http_response, - client=cast(Any, self._client), - ), - ) - - if self._stream_cls: - return cast( - R, - self._stream_cls( - cast_to=extract_stream_chunk_type(self._stream_cls), - response=self.http_response, - client=cast(Any, self._client), - ), - ) - - stream_cls = cast("type[Stream[Any]] | type[AsyncStream[Any]] | None", self._client._default_stream_cls) - if stream_cls is None: - raise MissingStreamClassError() - - return cast( - R, - stream_cls( - cast_to=cast_to, - response=self.http_response, - client=cast(Any, self._client), - ), - ) - - if cast_to is NoneType: - return cast(R, None) - - response = self.http_response - if cast_to == str: - return cast(R, response.text) - - if cast_to == bytes: - return cast(R, response.content) - - if cast_to == int: - return cast(R, int(response.text)) - - if cast_to == float: - return cast(R, float(response.text)) - - if cast_to == bool: - return cast(R, response.text.lower() == "true") - - if origin == APIResponse: - raise RuntimeError("Unexpected state - cast_to is `APIResponse`") - - if inspect.isclass(origin) and issubclass(origin, httpx.Response): - # Because of the invariance of our ResponseT TypeVar, users can subclass httpx.Response - # and pass that class to our request functions. We cannot change the variance to be either - # covariant or contravariant as that makes our usage of ResponseT illegal. We could construct - # the response class ourselves but that is something that should be supported directly in httpx - # as it would be easy to incorrectly construct the Response object due to the multitude of arguments. - if cast_to != httpx.Response: - raise ValueError(f"Subclasses of httpx.Response cannot be passed to `cast_to`") - return cast(R, response) - - if ( - inspect.isclass( - origin # pyright: ignore[reportUnknownArgumentType] - ) - and not issubclass(origin, BaseModel) - and issubclass(origin, pydantic.BaseModel) - ): - raise TypeError( - "Pydantic models must subclass our base model type, e.g. `from browser_use_sdk import BaseModel`" - ) - - if ( - cast_to is not object - and not origin is list - and not origin is dict - and not origin is Union - and not issubclass(origin, BaseModel) - ): - raise RuntimeError( - f"Unsupported type, expected {cast_to} to be a subclass of {BaseModel}, {dict}, {list}, {Union}, {NoneType}, {str} or {httpx.Response}." - ) - - # split is required to handle cases where additional information is included - # in the response, e.g. application/json; charset=utf-8 - content_type, *_ = response.headers.get("content-type", "*").split(";") - if not content_type.endswith("json"): - if is_basemodel(cast_to): - try: - data = response.json() - except Exception as exc: - log.debug("Could not read JSON from response data due to %s - %s", type(exc), exc) - else: - return self._client._process_response_data( - data=data, - cast_to=cast_to, # type: ignore - response=response, - ) - - if self._client._strict_response_validation: - raise APIResponseValidationError( - response=response, - message=f"Expected Content-Type response header to be `application/json` but received `{content_type}` instead.", - body=response.text, - ) - - # If the API responds with content that isn't JSON then we just return - # the (decoded) text without performing any parsing so that you can still - # handle the response however you need to. - return response.text # type: ignore - - data = response.json() - - return self._client._process_response_data( - data=data, - cast_to=cast_to, # type: ignore - response=response, - ) - - -class APIResponse(BaseAPIResponse[R]): - @overload - def parse(self, *, to: type[_T]) -> _T: ... - - @overload - def parse(self) -> R: ... - - def parse(self, *, to: type[_T] | None = None) -> R | _T: - """Returns the rich python representation of this response's data. - - For lower-level control, see `.read()`, `.json()`, `.iter_bytes()`. - - You can customise the type that the response is parsed into through - the `to` argument, e.g. - - ```py - from browser_use_sdk 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 browser_use_sdk 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 `browser_use_sdk._streaming` for reference", - ) - - -class StreamAlreadyConsumed(BrowserUseError): - """ - 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/browser_use_sdk/_streaming.py b/src/browser_use_sdk/_streaming.py deleted file mode 100644 index f52ffd4..0000000 --- a/src/browser_use_sdk/_streaming.py +++ /dev/null @@ -1,333 +0,0 @@ -# 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 BrowserUse, AsyncBrowserUse - - -_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: BrowserUse, - ) -> 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: AsyncBrowserUse, - ) -> 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/browser_use_sdk/_types.py b/src/browser_use_sdk/_types.py deleted file mode 100644 index 450c5bb..0000000 --- a/src/browser_use_sdk/_types.py +++ /dev/null @@ -1,219 +0,0 @@ -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 Set, 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 browser_use_sdk import NoneType -# client.get('/foo', cast_to=NoneType) -# -# or build it yourself: -# -# client.get('/foo', cast_to=type(None)) -if TYPE_CHECKING: - NoneType: Type[None] -else: - NoneType = type(None) - - -class RequestOptions(TypedDict, total=False): - headers: Headers - max_retries: int - timeout: float | Timeout | None - params: Query - extra_json: AnyMapping - idempotency_key: str - follow_redirects: bool - - -# Sentinel class used until PEP 0661 is accepted -class NotGiven: - """ - 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/6f31f8f68ef011f84357330186f603ff295312fd/pydantic/main.py#L79 -IncEx: TypeAlias = Union[Set[int], Set[str], Mapping[int, Union["IncEx", bool]], Mapping[str, Union["IncEx", bool]]] - -PostParser = Callable[[Any], Any] - - -@runtime_checkable -class InheritsGeneric(Protocol): - """Represents a type that has inherited from `Generic` - - The `__orig_bases__` property can be used to determine the resolved - type variable for a given base class. - """ - - __orig_bases__: tuple[_GenericAlias] - - -class _GenericAlias(Protocol): - __origin__: type[object] - - -class HttpxSendArgs(TypedDict, total=False): - auth: httpx.Auth - follow_redirects: bool diff --git a/src/browser_use_sdk/_utils/__init__.py b/src/browser_use_sdk/_utils/__init__.py deleted file mode 100644 index d4fda26..0000000 --- a/src/browser_use_sdk/_utils/__init__.py +++ /dev/null @@ -1,57 +0,0 @@ -from ._sync import asyncify as asyncify -from ._proxy import LazyProxy as LazyProxy -from ._utils import ( - flatten as flatten, - is_dict as is_dict, - is_list as is_list, - is_given as is_given, - is_tuple as is_tuple, - json_safe as json_safe, - lru_cache as lru_cache, - is_mapping as is_mapping, - is_tuple_t as is_tuple_t, - 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, - is_type_alias_type as is_type_alias_type, - strip_annotated_type as strip_annotated_type, - extract_type_var_from_base as extract_type_var_from_base, -) -from ._streams import consume_sync_iterator as consume_sync_iterator, consume_async_iterator as consume_async_iterator -from ._transform import ( - PropertyInfo as PropertyInfo, - transform as transform, - async_transform as async_transform, - maybe_transform as maybe_transform, - async_maybe_transform as async_maybe_transform, -) -from ._reflection import ( - function_has_argument as function_has_argument, - assert_signatures_in_sync as assert_signatures_in_sync, -) diff --git a/src/browser_use_sdk/_utils/_logs.py b/src/browser_use_sdk/_utils/_logs.py deleted file mode 100644 index 8398493..0000000 --- a/src/browser_use_sdk/_utils/_logs.py +++ /dev/null @@ -1,25 +0,0 @@ -import os -import logging - -logger: logging.Logger = logging.getLogger("browser_use_sdk") -httpx_logger: logging.Logger = logging.getLogger("httpx") - - -def _basic_config() -> None: - # e.g. [2023-10-05 14:12:26 - browser_use_sdk._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("BROWSER_USE_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/browser_use_sdk/_utils/_proxy.py b/src/browser_use_sdk/_utils/_proxy.py deleted file mode 100644 index 0f239a3..0000000 --- a/src/browser_use_sdk/_utils/_proxy.py +++ /dev/null @@ -1,65 +0,0 @@ -from __future__ import annotations - -from abc import ABC, abstractmethod -from typing import Generic, TypeVar, Iterable, cast -from typing_extensions import override - -T = TypeVar("T") - - -class LazyProxy(Generic[T], ABC): - """Implements data methods to pretend that an instance is another instance. - - This includes forwarding attribute access and other methods. - """ - - # Note: we have to special case proxies that themselves return proxies - # to support using a proxy as a catch-all for any random access, e.g. `proxy.foo.bar.baz` - - def __getattr__(self, attr: str) -> object: - proxied = self.__get_proxied__() - if isinstance(proxied, LazyProxy): - return proxied # pyright: ignore - return getattr(proxied, attr) - - @override - def __repr__(self) -> str: - proxied = self.__get_proxied__() - if isinstance(proxied, LazyProxy): - return proxied.__class__.__name__ - return repr(self.__get_proxied__()) - - @override - def __str__(self) -> str: - proxied = self.__get_proxied__() - if isinstance(proxied, LazyProxy): - return proxied.__class__.__name__ - return str(proxied) - - @override - def __dir__(self) -> Iterable[str]: - proxied = self.__get_proxied__() - if isinstance(proxied, LazyProxy): - return [] - return proxied.__dir__() - - @property # type: ignore - @override - def __class__(self) -> type: # pyright: ignore - try: - proxied = self.__get_proxied__() - except Exception: - return type(self) - if issubclass(type(proxied), LazyProxy): - return type(proxied) - return proxied.__class__ - - def __get_proxied__(self) -> T: - return self.__load__() - - def __as_proxied__(self) -> T: - """Helper method that returns the current proxy, typed as the loaded object""" - return cast(T, self) - - @abstractmethod - def __load__(self) -> T: ... diff --git a/src/browser_use_sdk/_utils/_reflection.py b/src/browser_use_sdk/_utils/_reflection.py deleted file mode 100644 index 89aa712..0000000 --- a/src/browser_use_sdk/_utils/_reflection.py +++ /dev/null @@ -1,42 +0,0 @@ -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/browser_use_sdk/_utils/_resources_proxy.py b/src/browser_use_sdk/_utils/_resources_proxy.py deleted file mode 100644 index 9451692..0000000 --- a/src/browser_use_sdk/_utils/_resources_proxy.py +++ /dev/null @@ -1,24 +0,0 @@ -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 `browser_use_sdk.resources` module. - - This is used so that we can lazily import `browser_use_sdk.resources` only when - needed *and* so that users can just import `browser_use_sdk` and reference `browser_use_sdk.resources` - """ - - @override - def __load__(self) -> Any: - import importlib - - mod = importlib.import_module("browser_use_sdk.resources") - return mod - - -resources = ResourcesProxy().__as_proxied__() diff --git a/src/browser_use_sdk/_utils/_streams.py b/src/browser_use_sdk/_utils/_streams.py deleted file mode 100644 index f4a0208..0000000 --- a/src/browser_use_sdk/_utils/_streams.py +++ /dev/null @@ -1,12 +0,0 @@ -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/browser_use_sdk/_utils/_sync.py b/src/browser_use_sdk/_utils/_sync.py deleted file mode 100644 index ad7ec71..0000000 --- a/src/browser_use_sdk/_utils/_sync.py +++ /dev/null @@ -1,86 +0,0 @@ -from __future__ import annotations - -import sys -import asyncio -import functools -import contextvars -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): - _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 anyio.to_thread.run_sync( - functools.partial(func, *args, **kwargs), - ) - - -# inspired by `asyncer`, https://github.com/tiangolo/asyncer -def asyncify(function: Callable[T_ParamSpec, T_Retval]) -> Callable[T_ParamSpec, Awaitable[T_Retval]]: - """ - Take a blocking function and create an async one that receives the same - positional and keyword arguments. 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. - - Usage: - - ```python - def blocking_func(arg1, arg2, kwarg1=None): - # blocking code - return result - - - result = asyncify(blocking_function)(arg1, arg2, kwarg1=value1) - ``` - - ## Arguments - - `function`: a blocking regular callable (e.g. a function) - - ## Return - - An async function that takes the same positional and keyword arguments as the - original one, that when called runs the same original function in a thread worker - and returns the result. - """ - - async def wrapper(*args: T_ParamSpec.args, **kwargs: T_ParamSpec.kwargs) -> T_Retval: - return await to_thread(function, *args, **kwargs) - - return wrapper diff --git a/src/browser_use_sdk/_utils/_transform.py b/src/browser_use_sdk/_utils/_transform.py deleted file mode 100644 index b0cc20a..0000000 --- a/src/browser_use_sdk/_utils/_transform.py +++ /dev/null @@ -1,447 +0,0 @@ -from __future__ import annotations - -import io -import base64 -import pathlib -from typing import Any, Mapping, TypeVar, cast -from datetime import date, datetime -from typing_extensions import Literal, get_args, override, get_type_hints as _get_type_hints - -import anyio -import pydantic - -from ._utils import ( - is_list, - is_given, - lru_cache, - is_mapping, - is_iterable, -) -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 get_origin, 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) - - -@lru_cache(maxsize=8096) -def _get_annotated_type(type_: type) -> type | None: - """If the given type is an `Annotated` type then it is returned, if not `None` is returned. - - This also unwraps the type when applicable, e.g. `Required[Annotated[T, ...]]` - """ - if is_required_type(type_): - # Unwrap `Required[Annotated[T, ...]]` to `Annotated[T, ...]` - type_ = get_args(type_)[0] - - if is_annotated_type(type_): - return type_ - - return None - - -def _maybe_transform_key(key: str, type_: type) -> str: - """Transform the given `data` based on the annotations provided in `type_`. - - Note: this function only looks at `Annotated` types that contain `PropertyInfo` metadata. - """ - annotated_type = _get_annotated_type(type_) - if annotated_type is None: - # no `Annotated` definition for this type, no transformation needed - return key - - # ignore the first argument as it is the actual type - annotations = get_args(annotated_type)[1:] - for annotation in annotations: - if isinstance(annotation, PropertyInfo) and annotation.alias is not None: - return annotation.alias - - return key - - -def _no_transform_needed(annotation: type) -> bool: - return annotation == float or annotation == int - - -def _transform_recursive( - data: object, - *, - annotation: type, - inner_type: type | None = None, -) -> object: - """Transform the given data against the expected type. - - Args: - annotation: The direct type annotation given to the particular piece of data. - This may or may not be wrapped in metadata types, e.g. `Required[T]`, `Annotated[T, ...]` etc - - inner_type: If applicable, this is the "inside" type. This is useful in certain cases where the outside type - is a container type such as `List[T]`. In that case `inner_type` should be set to `T` so that each entry in - the list can be transformed using the metadata from the container type. - - Defaults to the same value as the `annotation` argument. - """ - if inner_type is None: - inner_type = annotation - - stripped_type = strip_annotated_type(inner_type) - origin = get_origin(stripped_type) or stripped_type - if is_typeddict(stripped_type) and is_mapping(data): - return _transform_typeddict(data, stripped_type) - - if origin == dict and is_mapping(data): - items_type = get_args(stripped_type)[1] - return {key: _transform_recursive(value, annotation=items_type) for key, value in data.items()} - - if ( - # List[T] - (is_list_type(stripped_type) and is_list(data)) - # Iterable[T] - or (is_iterable_type(stripped_type) and is_iterable(data) and not isinstance(data, str)) - ): - # dicts are technically iterable, but it is an iterable on the keys of the dict and is not usually - # intended as an iterable, so we don't transform it. - if isinstance(data, dict): - return cast(object, data) - - inner_type = extract_type_arg(stripped_type, 0) - if _no_transform_needed(inner_type): - # for some types there is no need to transform anything, so we can get a small - # perf boost from skipping that work. - # - # but we still need to convert to a list to ensure the data is json-serializable - if is_list(data): - return data - return list(data) - - return [_transform_recursive(d, annotation=annotation, inner_type=inner_type) for d in data] - - if is_union_type(stripped_type): - # For union types we run the transformation against all subtypes to ensure that everything is transformed. - # - # TODO: there may be edge cases where the same normalized field name will transform to two different names - # in different subtypes. - for subtype in get_args(stripped_type): - data = _transform_recursive(data, annotation=annotation, inner_type=subtype) - return data - - if isinstance(data, pydantic.BaseModel): - return model_dump(data, exclude_unset=True, mode="json") - - annotated_type = _get_annotated_type(annotation) - if annotated_type is None: - return data - - # ignore the first argument as it is the actual type - annotations = get_args(annotated_type)[1:] - for annotation in annotations: - if isinstance(annotation, PropertyInfo) and annotation.format is not None: - return _format_data(data, annotation.format, annotation.format_template) - - return data - - -def _format_data(data: object, format_: PropertyFormat, format_template: str | None) -> object: - if isinstance(data, (date, datetime)): - if format_ == "iso8601": - return data.isoformat() - - if format_ == "custom" and format_template is not None: - return data.strftime(format_template) - - if format_ == "base64" and is_base64_file_input(data): - binary: str | bytes | None = None - - if isinstance(data, pathlib.Path): - binary = data.read_bytes() - elif isinstance(data, io.IOBase): - binary = data.read() - - if isinstance(binary, str): # type: ignore[unreachable] - binary = binary.encode() - - if not isinstance(binary, bytes): - raise RuntimeError(f"Could not read bytes from {data}; Received {type(binary)}") - - return base64.b64encode(binary).decode("ascii") - - return data - - -def _transform_typeddict( - data: Mapping[str, object], - expected_type: type, -) -> Mapping[str, object]: - result: dict[str, object] = {} - annotations = get_type_hints(expected_type, include_extras=True) - for key, value in data.items(): - if not is_given(value): - # we don't need to include `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 - 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) - origin = get_origin(stripped_type) or stripped_type - if is_typeddict(stripped_type) and is_mapping(data): - return await _async_transform_typeddict(data, stripped_type) - - if origin == dict and is_mapping(data): - items_type = get_args(stripped_type)[1] - return {key: _transform_recursive(value, annotation=items_type) for key, value in data.items()} - - if ( - # List[T] - (is_list_type(stripped_type) and is_list(data)) - # Iterable[T] - or (is_iterable_type(stripped_type) and is_iterable(data) and not isinstance(data, str)) - ): - # dicts are technically iterable, but it is an iterable on the keys of the dict and is not usually - # intended as an iterable, so we don't transform it. - if isinstance(data, dict): - return cast(object, data) - - inner_type = extract_type_arg(stripped_type, 0) - if _no_transform_needed(inner_type): - # for some types there is no need to transform anything, so we can get a small - # perf boost from skipping that work. - # - # but we still need to convert to a list to ensure the data is json-serializable - if is_list(data): - return data - return list(data) - - return [await _async_transform_recursive(d, annotation=annotation, inner_type=inner_type) for d in data] - - if is_union_type(stripped_type): - # For union types we run the transformation against all subtypes to ensure that everything is transformed. - # - # TODO: there may be edge cases where the same normalized field name will transform to two different names - # in different subtypes. - for subtype in get_args(stripped_type): - data = await _async_transform_recursive(data, annotation=annotation, inner_type=subtype) - return data - - if isinstance(data, pydantic.BaseModel): - return model_dump(data, exclude_unset=True, mode="json") - - annotated_type = _get_annotated_type(annotation) - if annotated_type is None: - return data - - # ignore the first argument as it is the actual type - annotations = get_args(annotated_type)[1:] - for annotation in annotations: - if isinstance(annotation, PropertyInfo) and annotation.format is not None: - return await _async_format_data(data, annotation.format, annotation.format_template) - - return data - - -async def _async_format_data(data: object, format_: PropertyFormat, format_template: str | None) -> object: - if isinstance(data, (date, datetime)): - if format_ == "iso8601": - return data.isoformat() - - if format_ == "custom" and format_template is not None: - return data.strftime(format_template) - - if format_ == "base64" and is_base64_file_input(data): - binary: str | bytes | None = None - - if isinstance(data, pathlib.Path): - binary = await anyio.Path(data).read_bytes() - elif isinstance(data, io.IOBase): - binary = data.read() - - if isinstance(binary, str): # type: ignore[unreachable] - binary = binary.encode() - - if not isinstance(binary, bytes): - raise RuntimeError(f"Could not read bytes from {data}; Received {type(binary)}") - - return base64.b64encode(binary).decode("ascii") - - return data - - -async def _async_transform_typeddict( - data: Mapping[str, object], - expected_type: type, -) -> Mapping[str, object]: - result: dict[str, object] = {} - annotations = get_type_hints(expected_type, include_extras=True) - for key, value in data.items(): - if not is_given(value): - # we don't need to include `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 - result[key] = value - else: - result[_maybe_transform_key(key, type_)] = await _async_transform_recursive(value, annotation=type_) - return result - - -@lru_cache(maxsize=8096) -def get_type_hints( - obj: Any, - globalns: dict[str, Any] | None = None, - localns: Mapping[str, Any] | None = None, - include_extras: bool = False, -) -> dict[str, Any]: - return _get_type_hints(obj, globalns=globalns, localns=localns, include_extras=include_extras) diff --git a/src/browser_use_sdk/_utils/_typing.py b/src/browser_use_sdk/_utils/_typing.py deleted file mode 100644 index 1bac954..0000000 --- a/src/browser_use_sdk/_utils/_typing.py +++ /dev/null @@ -1,151 +0,0 @@ -from __future__ import annotations - -import sys -import typing -import typing_extensions -from typing import Any, TypeVar, Iterable, cast -from collections import abc as _c_abc -from typing_extensions import ( - TypeIs, - Required, - Annotated, - get_args, - get_origin, -) - -from ._utils import lru_cache -from .._types import InheritsGeneric -from .._compat import is_union as _is_union - - -def is_annotated_type(typ: type) -> bool: - return get_origin(typ) == Annotated - - -def is_list_type(typ: type) -> bool: - return (get_origin(typ) or typ) == list - - -def is_iterable_type(typ: type) -> bool: - """If the given type is `typing.Iterable[T]`""" - origin = get_origin(typ) or typ - return origin == Iterable or origin == _c_abc.Iterable - - -def is_union_type(typ: type) -> bool: - return _is_union(get_origin(typ)) - - -def is_required_type(typ: type) -> bool: - return get_origin(typ) == Required - - -def is_typevar(typ: type) -> bool: - # type ignore is required because type checkers - # think this expression will always return False - return type(typ) == TypeVar # type: ignore - - -_TYPE_ALIAS_TYPES: tuple[type[typing_extensions.TypeAliasType], ...] = (typing_extensions.TypeAliasType,) -if sys.version_info >= (3, 12): - _TYPE_ALIAS_TYPES = (*_TYPE_ALIAS_TYPES, typing.TypeAliasType) - - -def is_type_alias_type(tp: Any, /) -> TypeIs[typing_extensions.TypeAliasType]: - """Return whether the provided argument is an instance of `TypeAliasType`. - - ```python - type Int = int - is_type_alias_type(Int) - # > True - Str = TypeAliasType("Str", str) - is_type_alias_type(Str) - # > True - ``` - """ - return isinstance(tp, _TYPE_ALIAS_TYPES) - - -# Extracts T from Annotated[T, ...] or from Required[Annotated[T, ...]] -@lru_cache(maxsize=8096) -def strip_annotated_type(typ: type) -> type: - if is_required_type(typ) or is_annotated_type(typ): - return strip_annotated_type(cast(type, get_args(typ)[0])) - - return typ - - -def extract_type_arg(typ: type, index: int) -> type: - args = get_args(typ) - try: - return cast(type, args[index]) - except IndexError as err: - raise RuntimeError(f"Expected type {typ} to have a type argument at index {index} but it did not") from err - - -def extract_type_var_from_base( - typ: type, - *, - generic_bases: tuple[type, ...], - index: int, - failure_message: str | None = None, -) -> type: - """Given a type like `Foo[T]`, returns the generic type variable `T`. - - This also handles the case where a concrete subclass is given, e.g. - ```py - class MyResponse(Foo[bytes]): - ... - - extract_type_var(MyResponse, bases=(Foo,), index=0) -> bytes - ``` - - And where a generic subclass is given: - ```py - _T = TypeVar('_T') - class MyResponse(Foo[_T]): - ... - - extract_type_var(MyResponse[bytes], bases=(Foo,), index=0) -> bytes - ``` - """ - cls = cast(object, get_origin(typ) or typ) - if cls in generic_bases: # pyright: ignore[reportUnnecessaryContains] - # we're given the class directly - return extract_type_arg(typ, index) - - # if a subclass is given - # --- - # this is needed as __orig_bases__ is not present in the typeshed stubs - # because it is intended to be for internal use only, however there does - # not seem to be a way to resolve generic TypeVars for inherited subclasses - # without using it. - if isinstance(cls, InheritsGeneric): - target_base_class: Any | None = None - for base in cls.__orig_bases__: - if base.__origin__ in generic_bases: - target_base_class = base - break - - if target_base_class is None: - raise RuntimeError( - "Could not find the generic base class;\n" - "This should never happen;\n" - f"Does {cls} inherit from one of {generic_bases} ?" - ) - - extracted = extract_type_arg(target_base_class, index) - if is_typevar(extracted): - # If the extracted type argument is itself a type variable - # then that means the subclass itself is generic, so we have - # to resolve the type argument from the class itself, not - # the base class. - # - # Note: if there is more than 1 type argument, the subclass could - # change the ordering of the type arguments, this is not currently - # supported. - return extract_type_arg(typ, index) - - return extracted - - raise RuntimeError(failure_message or f"Could not resolve inner type variable at index {index} for {typ}") diff --git a/src/browser_use_sdk/_utils/_utils.py b/src/browser_use_sdk/_utils/_utils.py deleted file mode 100644 index ea3cf3f..0000000 --- a/src/browser_use_sdk/_utils/_utils.py +++ /dev/null @@ -1,422 +0,0 @@ -from __future__ import annotations - -import os -import re -import inspect -import functools -from typing import ( - Any, - Tuple, - Mapping, - TypeVar, - Callable, - Iterable, - Sequence, - cast, - overload, -) -from pathlib import Path -from datetime import date, datetime -from typing_extensions import TypeGuard - -import sniffio - -from .._types import 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 flattened_key is not None - - if is_list(obj): - files: list[tuple[str, FileTypes]] = [] - for entry in obj: - assert_is_file_content(entry, key=flattened_key + "[]" if flattened_key else "") - files.append((flattened_key + "[]", cast(FileTypes, entry))) - return files - - assert_is_file_content(obj, key=flattened_key) - return [(flattened_key, cast(FileTypes, obj))] - - index += 1 - if is_dict(obj): - try: - # We are at the last entry in the path so we must remove the field - if (len(path)) == index: - item = obj.pop(key) - else: - item = obj[key] - except KeyError: - # Key was not present in the dictionary, this is not indicative of an error - # as the given path may not point to a required field. We also do not want - # to enforce required fields as the API may differ from the spec in some cases. - return [] - if flattened_key is None: - flattened_key = key - else: - flattened_key += f"[{key}]" - return _extract_items( - item, - path, - index=index, - flattened_key=flattened_key, - ) - elif is_list(obj): - if key != "": - return [] - - return flatten( - [ - _extract_items( - item, - path, - index=index, - flattened_key=flattened_key + "[]" if flattened_key is not None else "[]", - ) - for item in obj - ] - ) - - # Something unexpected was passed, just ignore it. - return [] - - -def is_given(obj: 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] - - -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/browser_use_sdk/_version.py b/src/browser_use_sdk/_version.py deleted file mode 100644 index dbca030..0000000 --- a/src/browser_use_sdk/_version.py +++ /dev/null @@ -1,4 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -__title__ = "browser_use_sdk" -__version__ = "1.0.2" # x-release-please-version diff --git a/src/browser_use_sdk/lib/.keep b/src/browser_use_sdk/lib/.keep deleted file mode 100644 index 5e2c99f..0000000 --- a/src/browser_use_sdk/lib/.keep +++ /dev/null @@ -1,4 +0,0 @@ -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/browser_use_sdk/lib/parse.py b/src/browser_use_sdk/lib/parse.py deleted file mode 100644 index b11e44e..0000000 --- a/src/browser_use_sdk/lib/parse.py +++ /dev/null @@ -1,35 +0,0 @@ -import json -import hashlib -from typing import Any, Union, Generic, TypeVar -from datetime import datetime - -from pydantic import BaseModel - -from browser_use_sdk.types.task_view import TaskView - -T = TypeVar("T", bound=BaseModel) - - -class TaskViewWithOutput(TaskView, Generic[T]): - """ - TaskView with structured output. - """ - - parsed_output: Union[T, None] - - -class CustomJSONEncoder(json.JSONEncoder): - """Custom JSON encoder to handle datetime objects.""" - - # NOTE: Python doesn't have the override decorator in 3.8, that's why we ignore it. - def default(self, o: Any) -> Any: # type: ignore[override] - if isinstance(o, datetime): - return o.isoformat() - return super().default(o) - - -def hash_task_view(task_view: TaskView) -> str: - """Hashes the task view to detect changes.""" - return hashlib.sha256( - json.dumps(task_view.model_dump(), sort_keys=True, cls=CustomJSONEncoder).encode() - ).hexdigest() diff --git a/src/browser_use_sdk/resources/__init__.py b/src/browser_use_sdk/resources/__init__.py deleted file mode 100644 index c8d13bb..0000000 --- a/src/browser_use_sdk/resources/__init__.py +++ /dev/null @@ -1,75 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from .tasks import ( - TasksResource, - AsyncTasksResource, - TasksResourceWithRawResponse, - AsyncTasksResourceWithRawResponse, - TasksResourceWithStreamingResponse, - AsyncTasksResourceWithStreamingResponse, -) -from .users import ( - UsersResource, - AsyncUsersResource, - UsersResourceWithRawResponse, - AsyncUsersResourceWithRawResponse, - UsersResourceWithStreamingResponse, - AsyncUsersResourceWithStreamingResponse, -) -from .sessions import ( - SessionsResource, - AsyncSessionsResource, - SessionsResourceWithRawResponse, - AsyncSessionsResourceWithRawResponse, - SessionsResourceWithStreamingResponse, - AsyncSessionsResourceWithStreamingResponse, -) -from .agent_profiles import ( - AgentProfilesResource, - AsyncAgentProfilesResource, - AgentProfilesResourceWithRawResponse, - AsyncAgentProfilesResourceWithRawResponse, - AgentProfilesResourceWithStreamingResponse, - AsyncAgentProfilesResourceWithStreamingResponse, -) -from .browser_profiles import ( - BrowserProfilesResource, - AsyncBrowserProfilesResource, - BrowserProfilesResourceWithRawResponse, - AsyncBrowserProfilesResourceWithRawResponse, - BrowserProfilesResourceWithStreamingResponse, - AsyncBrowserProfilesResourceWithStreamingResponse, -) - -__all__ = [ - "UsersResource", - "AsyncUsersResource", - "UsersResourceWithRawResponse", - "AsyncUsersResourceWithRawResponse", - "UsersResourceWithStreamingResponse", - "AsyncUsersResourceWithStreamingResponse", - "TasksResource", - "AsyncTasksResource", - "TasksResourceWithRawResponse", - "AsyncTasksResourceWithRawResponse", - "TasksResourceWithStreamingResponse", - "AsyncTasksResourceWithStreamingResponse", - "SessionsResource", - "AsyncSessionsResource", - "SessionsResourceWithRawResponse", - "AsyncSessionsResourceWithRawResponse", - "SessionsResourceWithStreamingResponse", - "AsyncSessionsResourceWithStreamingResponse", - "BrowserProfilesResource", - "AsyncBrowserProfilesResource", - "BrowserProfilesResourceWithRawResponse", - "AsyncBrowserProfilesResourceWithRawResponse", - "BrowserProfilesResourceWithStreamingResponse", - "AsyncBrowserProfilesResourceWithStreamingResponse", - "AgentProfilesResource", - "AsyncAgentProfilesResource", - "AgentProfilesResourceWithRawResponse", - "AsyncAgentProfilesResourceWithRawResponse", - "AgentProfilesResourceWithStreamingResponse", - "AsyncAgentProfilesResourceWithStreamingResponse", -] diff --git a/src/browser_use_sdk/resources/agent_profiles.py b/src/browser_use_sdk/resources/agent_profiles.py deleted file mode 100644 index b596dbc..0000000 --- a/src/browser_use_sdk/resources/agent_profiles.py +++ /dev/null @@ -1,748 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -from typing import List, Optional - -import httpx - -from ..types import agent_profile_list_params, agent_profile_create_params, agent_profile_update_params -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 -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.agent_profile_view import AgentProfileView -from ..types.agent_profile_list_response import AgentProfileListResponse - -__all__ = ["AgentProfilesResource", "AsyncAgentProfilesResource"] - - -class AgentProfilesResource(SyncAPIResource): - @cached_property - def with_raw_response(self) -> AgentProfilesResourceWithRawResponse: - """ - 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/browser-use/browser-use-python#accessing-raw-response-data-eg-headers - """ - return AgentProfilesResourceWithRawResponse(self) - - @cached_property - def with_streaming_response(self) -> AgentProfilesResourceWithStreamingResponse: - """ - An alternative to `.with_raw_response` that doesn't eagerly read the response body. - - For more information, see https://www.github.com/browser-use/browser-use-python#with_streaming_response - """ - return AgentProfilesResourceWithStreamingResponse(self) - - def create( - self, - *, - name: str, - allowed_domains: List[str] | NotGiven = NOT_GIVEN, - custom_system_prompt_extension: str | NotGiven = NOT_GIVEN, - description: str | NotGiven = NOT_GIVEN, - flash_mode: bool | NotGiven = NOT_GIVEN, - highlight_elements: bool | NotGiven = NOT_GIVEN, - max_agent_steps: int | NotGiven = NOT_GIVEN, - thinking: bool | NotGiven = NOT_GIVEN, - vision: bool | 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, - ) -> AgentProfileView: - """ - Create a new agent profile for the authenticated user. - - Agent profiles define how your AI agents behave during tasks. You can create - multiple profiles for different use cases (e.g., customer support, data - analysis, web scraping). Free users can create 1 profile; paid users can create - unlimited profiles. - - Key features you can configure: - - - System prompt: The core instructions that define the agent's personality and - behavior - - Allowed domains: Restrict which websites the agent can access - - Max steps: Limit how many actions the agent can take in a single task - - Vision: Enable/disable the agent's ability to see and analyze screenshots - - Thinking: Enable/disable the agent's reasoning process - - Args: - - - request: The agent profile configuration including name, description, and - behavior settings - - Returns: - - - The newly created agent profile with all its details - - Raises: - - - 402: If user needs a subscription to create additional profiles - - 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._post( - "/agent-profiles", - body=maybe_transform( - { - "name": name, - "allowed_domains": allowed_domains, - "custom_system_prompt_extension": custom_system_prompt_extension, - "description": description, - "flash_mode": flash_mode, - "highlight_elements": highlight_elements, - "max_agent_steps": max_agent_steps, - "thinking": thinking, - "vision": vision, - }, - agent_profile_create_params.AgentProfileCreateParams, - ), - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=AgentProfileView, - ) - - def retrieve( - self, - profile_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, - ) -> AgentProfileView: - """ - Get a specific agent profile by its ID. - - Retrieves the complete details of an agent profile, including all its - configuration settings like system prompts, allowed domains, and behavior flags. - - Args: - - - profile_id: The unique identifier of the agent profile - - Returns: - - - Complete agent profile information - - Raises: - - - 404: If the user agent profile doesn't exist - - 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 profile_id: - raise ValueError(f"Expected a non-empty value for `profile_id` but received {profile_id!r}") - return self._get( - f"/agent-profiles/{profile_id}", - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=AgentProfileView, - ) - - def update( - self, - profile_id: str, - *, - allowed_domains: Optional[List[str]] | NotGiven = NOT_GIVEN, - custom_system_prompt_extension: Optional[str] | NotGiven = NOT_GIVEN, - description: Optional[str] | NotGiven = NOT_GIVEN, - flash_mode: Optional[bool] | NotGiven = NOT_GIVEN, - highlight_elements: Optional[bool] | NotGiven = NOT_GIVEN, - max_agent_steps: Optional[int] | NotGiven = NOT_GIVEN, - name: Optional[str] | NotGiven = NOT_GIVEN, - thinking: Optional[bool] | NotGiven = NOT_GIVEN, - vision: Optional[bool] | 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, - ) -> AgentProfileView: - """ - Update an existing agent profile. - - Modify any aspect of an agent profile, such as its name, description, system - prompt, or behavior settings. Only the fields you provide will be updated; other - fields remain unchanged. - - Args: - - - profile_id: The unique identifier of the agent profile to update - - request: The fields to update (only provided fields will be changed) - - Returns: - - - The updated agent profile with all its current details - - Raises: - - - 404: If the user agent profile doesn't exist - - 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 profile_id: - raise ValueError(f"Expected a non-empty value for `profile_id` but received {profile_id!r}") - return self._patch( - f"/agent-profiles/{profile_id}", - body=maybe_transform( - { - "allowed_domains": allowed_domains, - "custom_system_prompt_extension": custom_system_prompt_extension, - "description": description, - "flash_mode": flash_mode, - "highlight_elements": highlight_elements, - "max_agent_steps": max_agent_steps, - "name": name, - "thinking": thinking, - "vision": vision, - }, - agent_profile_update_params.AgentProfileUpdateParams, - ), - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=AgentProfileView, - ) - - def list( - self, - *, - page_number: int | NotGiven = NOT_GIVEN, - page_size: 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, - ) -> AgentProfileListResponse: - """ - Get a paginated list of all agent profiles for the authenticated user. - - Agent profiles define how your AI agents behave, including their personality, - capabilities, and limitations. Use this endpoint to see all your configured - agent profiles. - - Returns: - - - A paginated list of agent profiles - - Total count of profiles - - Page information for navigation - - 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( - "/agent-profiles", - options=make_request_options( - extra_headers=extra_headers, - extra_query=extra_query, - extra_body=extra_body, - timeout=timeout, - query=maybe_transform( - { - "page_number": page_number, - "page_size": page_size, - }, - agent_profile_list_params.AgentProfileListParams, - ), - ), - cast_to=AgentProfileListResponse, - ) - - def delete( - self, - profile_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 an agent profile. - - Permanently removes an agent profile and all its configuration. This action - cannot be undone. Any tasks that were using this profile will continue to work, - but you won't be able to create new tasks with the deleted profile. - - Args: - - - profile_id: The unique identifier of the agent profile to delete - - Returns: - - - 204 No Content on successful deletion (idempotent) - - 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 profile_id: - raise ValueError(f"Expected a non-empty value for `profile_id` but received {profile_id!r}") - extra_headers = {"Accept": "*/*", **(extra_headers or {})} - return self._delete( - f"/agent-profiles/{profile_id}", - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=NoneType, - ) - - -class AsyncAgentProfilesResource(AsyncAPIResource): - @cached_property - def with_raw_response(self) -> AsyncAgentProfilesResourceWithRawResponse: - """ - 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/browser-use/browser-use-python#accessing-raw-response-data-eg-headers - """ - return AsyncAgentProfilesResourceWithRawResponse(self) - - @cached_property - def with_streaming_response(self) -> AsyncAgentProfilesResourceWithStreamingResponse: - """ - An alternative to `.with_raw_response` that doesn't eagerly read the response body. - - For more information, see https://www.github.com/browser-use/browser-use-python#with_streaming_response - """ - return AsyncAgentProfilesResourceWithStreamingResponse(self) - - async def create( - self, - *, - name: str, - allowed_domains: List[str] | NotGiven = NOT_GIVEN, - custom_system_prompt_extension: str | NotGiven = NOT_GIVEN, - description: str | NotGiven = NOT_GIVEN, - flash_mode: bool | NotGiven = NOT_GIVEN, - highlight_elements: bool | NotGiven = NOT_GIVEN, - max_agent_steps: int | NotGiven = NOT_GIVEN, - thinking: bool | NotGiven = NOT_GIVEN, - vision: bool | 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, - ) -> AgentProfileView: - """ - Create a new agent profile for the authenticated user. - - Agent profiles define how your AI agents behave during tasks. You can create - multiple profiles for different use cases (e.g., customer support, data - analysis, web scraping). Free users can create 1 profile; paid users can create - unlimited profiles. - - Key features you can configure: - - - System prompt: The core instructions that define the agent's personality and - behavior - - Allowed domains: Restrict which websites the agent can access - - Max steps: Limit how many actions the agent can take in a single task - - Vision: Enable/disable the agent's ability to see and analyze screenshots - - Thinking: Enable/disable the agent's reasoning process - - Args: - - - request: The agent profile configuration including name, description, and - behavior settings - - Returns: - - - The newly created agent profile with all its details - - Raises: - - - 402: If user needs a subscription to create additional profiles - - 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._post( - "/agent-profiles", - body=await async_maybe_transform( - { - "name": name, - "allowed_domains": allowed_domains, - "custom_system_prompt_extension": custom_system_prompt_extension, - "description": description, - "flash_mode": flash_mode, - "highlight_elements": highlight_elements, - "max_agent_steps": max_agent_steps, - "thinking": thinking, - "vision": vision, - }, - agent_profile_create_params.AgentProfileCreateParams, - ), - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=AgentProfileView, - ) - - async def retrieve( - self, - profile_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, - ) -> AgentProfileView: - """ - Get a specific agent profile by its ID. - - Retrieves the complete details of an agent profile, including all its - configuration settings like system prompts, allowed domains, and behavior flags. - - Args: - - - profile_id: The unique identifier of the agent profile - - Returns: - - - Complete agent profile information - - Raises: - - - 404: If the user agent profile doesn't exist - - 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 profile_id: - raise ValueError(f"Expected a non-empty value for `profile_id` but received {profile_id!r}") - return await self._get( - f"/agent-profiles/{profile_id}", - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=AgentProfileView, - ) - - async def update( - self, - profile_id: str, - *, - allowed_domains: Optional[List[str]] | NotGiven = NOT_GIVEN, - custom_system_prompt_extension: Optional[str] | NotGiven = NOT_GIVEN, - description: Optional[str] | NotGiven = NOT_GIVEN, - flash_mode: Optional[bool] | NotGiven = NOT_GIVEN, - highlight_elements: Optional[bool] | NotGiven = NOT_GIVEN, - max_agent_steps: Optional[int] | NotGiven = NOT_GIVEN, - name: Optional[str] | NotGiven = NOT_GIVEN, - thinking: Optional[bool] | NotGiven = NOT_GIVEN, - vision: Optional[bool] | 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, - ) -> AgentProfileView: - """ - Update an existing agent profile. - - Modify any aspect of an agent profile, such as its name, description, system - prompt, or behavior settings. Only the fields you provide will be updated; other - fields remain unchanged. - - Args: - - - profile_id: The unique identifier of the agent profile to update - - request: The fields to update (only provided fields will be changed) - - Returns: - - - The updated agent profile with all its current details - - Raises: - - - 404: If the user agent profile doesn't exist - - 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 profile_id: - raise ValueError(f"Expected a non-empty value for `profile_id` but received {profile_id!r}") - return await self._patch( - f"/agent-profiles/{profile_id}", - body=await async_maybe_transform( - { - "allowed_domains": allowed_domains, - "custom_system_prompt_extension": custom_system_prompt_extension, - "description": description, - "flash_mode": flash_mode, - "highlight_elements": highlight_elements, - "max_agent_steps": max_agent_steps, - "name": name, - "thinking": thinking, - "vision": vision, - }, - agent_profile_update_params.AgentProfileUpdateParams, - ), - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=AgentProfileView, - ) - - async def list( - self, - *, - page_number: int | NotGiven = NOT_GIVEN, - page_size: 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, - ) -> AgentProfileListResponse: - """ - Get a paginated list of all agent profiles for the authenticated user. - - Agent profiles define how your AI agents behave, including their personality, - capabilities, and limitations. Use this endpoint to see all your configured - agent profiles. - - Returns: - - - A paginated list of agent profiles - - Total count of profiles - - Page information for navigation - - 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( - "/agent-profiles", - options=make_request_options( - extra_headers=extra_headers, - extra_query=extra_query, - extra_body=extra_body, - timeout=timeout, - query=await async_maybe_transform( - { - "page_number": page_number, - "page_size": page_size, - }, - agent_profile_list_params.AgentProfileListParams, - ), - ), - cast_to=AgentProfileListResponse, - ) - - async def delete( - self, - profile_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 an agent profile. - - Permanently removes an agent profile and all its configuration. This action - cannot be undone. Any tasks that were using this profile will continue to work, - but you won't be able to create new tasks with the deleted profile. - - Args: - - - profile_id: The unique identifier of the agent profile to delete - - Returns: - - - 204 No Content on successful deletion (idempotent) - - 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 profile_id: - raise ValueError(f"Expected a non-empty value for `profile_id` but received {profile_id!r}") - extra_headers = {"Accept": "*/*", **(extra_headers or {})} - return await self._delete( - f"/agent-profiles/{profile_id}", - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=NoneType, - ) - - -class AgentProfilesResourceWithRawResponse: - def __init__(self, agent_profiles: AgentProfilesResource) -> None: - self._agent_profiles = agent_profiles - - self.create = to_raw_response_wrapper( - agent_profiles.create, - ) - self.retrieve = to_raw_response_wrapper( - agent_profiles.retrieve, - ) - self.update = to_raw_response_wrapper( - agent_profiles.update, - ) - self.list = to_raw_response_wrapper( - agent_profiles.list, - ) - self.delete = to_raw_response_wrapper( - agent_profiles.delete, - ) - - -class AsyncAgentProfilesResourceWithRawResponse: - def __init__(self, agent_profiles: AsyncAgentProfilesResource) -> None: - self._agent_profiles = agent_profiles - - self.create = async_to_raw_response_wrapper( - agent_profiles.create, - ) - self.retrieve = async_to_raw_response_wrapper( - agent_profiles.retrieve, - ) - self.update = async_to_raw_response_wrapper( - agent_profiles.update, - ) - self.list = async_to_raw_response_wrapper( - agent_profiles.list, - ) - self.delete = async_to_raw_response_wrapper( - agent_profiles.delete, - ) - - -class AgentProfilesResourceWithStreamingResponse: - def __init__(self, agent_profiles: AgentProfilesResource) -> None: - self._agent_profiles = agent_profiles - - self.create = to_streamed_response_wrapper( - agent_profiles.create, - ) - self.retrieve = to_streamed_response_wrapper( - agent_profiles.retrieve, - ) - self.update = to_streamed_response_wrapper( - agent_profiles.update, - ) - self.list = to_streamed_response_wrapper( - agent_profiles.list, - ) - self.delete = to_streamed_response_wrapper( - agent_profiles.delete, - ) - - -class AsyncAgentProfilesResourceWithStreamingResponse: - def __init__(self, agent_profiles: AsyncAgentProfilesResource) -> None: - self._agent_profiles = agent_profiles - - self.create = async_to_streamed_response_wrapper( - agent_profiles.create, - ) - self.retrieve = async_to_streamed_response_wrapper( - agent_profiles.retrieve, - ) - self.update = async_to_streamed_response_wrapper( - agent_profiles.update, - ) - self.list = async_to_streamed_response_wrapper( - agent_profiles.list, - ) - self.delete = async_to_streamed_response_wrapper( - agent_profiles.delete, - ) diff --git a/src/browser_use_sdk/resources/browser_profiles.py b/src/browser_use_sdk/resources/browser_profiles.py deleted file mode 100644 index 3a8b417..0000000 --- a/src/browser_use_sdk/resources/browser_profiles.py +++ /dev/null @@ -1,764 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -from typing import Optional - -import httpx - -from ..types import ( - ProxyCountryCode, - browser_profile_list_params, - browser_profile_create_params, - browser_profile_update_params, -) -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 -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.proxy_country_code import ProxyCountryCode -from ..types.browser_profile_view import BrowserProfileView -from ..types.browser_profile_list_response import BrowserProfileListResponse - -__all__ = ["BrowserProfilesResource", "AsyncBrowserProfilesResource"] - - -class BrowserProfilesResource(SyncAPIResource): - @cached_property - def with_raw_response(self) -> BrowserProfilesResourceWithRawResponse: - """ - 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/browser-use/browser-use-python#accessing-raw-response-data-eg-headers - """ - return BrowserProfilesResourceWithRawResponse(self) - - @cached_property - def with_streaming_response(self) -> BrowserProfilesResourceWithStreamingResponse: - """ - An alternative to `.with_raw_response` that doesn't eagerly read the response body. - - For more information, see https://www.github.com/browser-use/browser-use-python#with_streaming_response - """ - return BrowserProfilesResourceWithStreamingResponse(self) - - def create( - self, - *, - name: str, - ad_blocker: bool | NotGiven = NOT_GIVEN, - browser_viewport_height: int | NotGiven = NOT_GIVEN, - browser_viewport_width: int | NotGiven = NOT_GIVEN, - description: str | NotGiven = NOT_GIVEN, - is_mobile: bool | NotGiven = NOT_GIVEN, - persist: bool | NotGiven = NOT_GIVEN, - proxy: bool | NotGiven = NOT_GIVEN, - proxy_country_code: ProxyCountryCode | NotGiven = NOT_GIVEN, - store_cache: bool | 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, - ) -> BrowserProfileView: - """ - Create a new browser profile for the authenticated user. - - Browser profiles define how your web browsers behave during AI agent tasks. You - can create multiple profiles for different use cases (e.g., mobile testing, - desktop browsing, proxy-enabled scraping). Free users can create up to 10 - profiles; paid users can create unlimited profiles. - - Key features you can configure: - - - Viewport dimensions: Set the browser window size for consistent rendering - - Mobile emulation: Enable mobile device simulation - - Proxy settings: Route traffic through specific locations or proxy servers - - Ad blocking: Enable/disable ad blocking for cleaner browsing - - Cache persistence: Choose whether to save browser data between sessions - - Args: - - - request: The browser profile configuration including name, description, and - browser settings - - Returns: - - - The newly created browser profile with all its details - - Raises: - - - 402: If user needs a subscription to create additional profiles - - 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._post( - "/browser-profiles", - body=maybe_transform( - { - "name": name, - "ad_blocker": ad_blocker, - "browser_viewport_height": browser_viewport_height, - "browser_viewport_width": browser_viewport_width, - "description": description, - "is_mobile": is_mobile, - "persist": persist, - "proxy": proxy, - "proxy_country_code": proxy_country_code, - "store_cache": store_cache, - }, - browser_profile_create_params.BrowserProfileCreateParams, - ), - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=BrowserProfileView, - ) - - def retrieve( - self, - profile_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, - ) -> BrowserProfileView: - """ - Get a specific browser profile by its ID. - - Retrieves the complete details of a browser profile, including all its - configuration settings like viewport dimensions, proxy settings, and behavior - flags. - - Args: - - - profile_id: The unique identifier of the browser profile - - Returns: - - - Complete browser profile information - - Raises: - - - 404: If the user browser profile doesn't exist - - 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 profile_id: - raise ValueError(f"Expected a non-empty value for `profile_id` but received {profile_id!r}") - return self._get( - f"/browser-profiles/{profile_id}", - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=BrowserProfileView, - ) - - def update( - self, - profile_id: str, - *, - ad_blocker: Optional[bool] | NotGiven = NOT_GIVEN, - browser_viewport_height: Optional[int] | NotGiven = NOT_GIVEN, - browser_viewport_width: Optional[int] | NotGiven = NOT_GIVEN, - description: Optional[str] | NotGiven = NOT_GIVEN, - is_mobile: Optional[bool] | NotGiven = NOT_GIVEN, - name: Optional[str] | NotGiven = NOT_GIVEN, - persist: Optional[bool] | NotGiven = NOT_GIVEN, - proxy: Optional[bool] | NotGiven = NOT_GIVEN, - proxy_country_code: Optional[ProxyCountryCode] | NotGiven = NOT_GIVEN, - store_cache: Optional[bool] | 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, - ) -> BrowserProfileView: - """ - Update an existing browser profile. - - Modify any aspect of a browser profile, such as its name, description, viewport - settings, or proxy configuration. Only the fields you provide will be updated; - other fields remain unchanged. - - Args: - - - profile_id: The unique identifier of the browser profile to update - - request: The fields to update (only provided fields will be changed) - - Returns: - - - The updated browser profile with all its current details - - Raises: - - - 404: If the user browser profile doesn't exist - - 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 profile_id: - raise ValueError(f"Expected a non-empty value for `profile_id` but received {profile_id!r}") - return self._patch( - f"/browser-profiles/{profile_id}", - body=maybe_transform( - { - "ad_blocker": ad_blocker, - "browser_viewport_height": browser_viewport_height, - "browser_viewport_width": browser_viewport_width, - "description": description, - "is_mobile": is_mobile, - "name": name, - "persist": persist, - "proxy": proxy, - "proxy_country_code": proxy_country_code, - "store_cache": store_cache, - }, - browser_profile_update_params.BrowserProfileUpdateParams, - ), - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=BrowserProfileView, - ) - - def list( - self, - *, - page_number: int | NotGiven = NOT_GIVEN, - page_size: 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, - ) -> BrowserProfileListResponse: - """ - Get a paginated list of all browser profiles for the authenticated user. - - Browser profiles define how your web browsers behave during AI agent tasks, - including settings like viewport size, mobile emulation, proxy configuration, - and ad blocking. Use this endpoint to see all your configured browser profiles. - - Returns: - - - A paginated list of browser profiles - - Total count of profiles - - Page information for navigation - - 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( - "/browser-profiles", - options=make_request_options( - extra_headers=extra_headers, - extra_query=extra_query, - extra_body=extra_body, - timeout=timeout, - query=maybe_transform( - { - "page_number": page_number, - "page_size": page_size, - }, - browser_profile_list_params.BrowserProfileListParams, - ), - ), - cast_to=BrowserProfileListResponse, - ) - - def delete( - self, - profile_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 browser profile. - - Permanently removes a browser profile and all its configuration. This action - cannot be undone. The profile will also be removed from the browser service. Any - active sessions using this profile will continue to work, but you won't be able - to create new sessions with the deleted profile. - - Args: - - - profile_id: The unique identifier of the browser profile to delete - - Returns: - - - 204 No Content on successful deletion (idempotent) - - 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 profile_id: - raise ValueError(f"Expected a non-empty value for `profile_id` but received {profile_id!r}") - extra_headers = {"Accept": "*/*", **(extra_headers or {})} - return self._delete( - f"/browser-profiles/{profile_id}", - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=NoneType, - ) - - -class AsyncBrowserProfilesResource(AsyncAPIResource): - @cached_property - def with_raw_response(self) -> AsyncBrowserProfilesResourceWithRawResponse: - """ - 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/browser-use/browser-use-python#accessing-raw-response-data-eg-headers - """ - return AsyncBrowserProfilesResourceWithRawResponse(self) - - @cached_property - def with_streaming_response(self) -> AsyncBrowserProfilesResourceWithStreamingResponse: - """ - An alternative to `.with_raw_response` that doesn't eagerly read the response body. - - For more information, see https://www.github.com/browser-use/browser-use-python#with_streaming_response - """ - return AsyncBrowserProfilesResourceWithStreamingResponse(self) - - async def create( - self, - *, - name: str, - ad_blocker: bool | NotGiven = NOT_GIVEN, - browser_viewport_height: int | NotGiven = NOT_GIVEN, - browser_viewport_width: int | NotGiven = NOT_GIVEN, - description: str | NotGiven = NOT_GIVEN, - is_mobile: bool | NotGiven = NOT_GIVEN, - persist: bool | NotGiven = NOT_GIVEN, - proxy: bool | NotGiven = NOT_GIVEN, - proxy_country_code: ProxyCountryCode | NotGiven = NOT_GIVEN, - store_cache: bool | 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, - ) -> BrowserProfileView: - """ - Create a new browser profile for the authenticated user. - - Browser profiles define how your web browsers behave during AI agent tasks. You - can create multiple profiles for different use cases (e.g., mobile testing, - desktop browsing, proxy-enabled scraping). Free users can create up to 10 - profiles; paid users can create unlimited profiles. - - Key features you can configure: - - - Viewport dimensions: Set the browser window size for consistent rendering - - Mobile emulation: Enable mobile device simulation - - Proxy settings: Route traffic through specific locations or proxy servers - - Ad blocking: Enable/disable ad blocking for cleaner browsing - - Cache persistence: Choose whether to save browser data between sessions - - Args: - - - request: The browser profile configuration including name, description, and - browser settings - - Returns: - - - The newly created browser profile with all its details - - Raises: - - - 402: If user needs a subscription to create additional profiles - - 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._post( - "/browser-profiles", - body=await async_maybe_transform( - { - "name": name, - "ad_blocker": ad_blocker, - "browser_viewport_height": browser_viewport_height, - "browser_viewport_width": browser_viewport_width, - "description": description, - "is_mobile": is_mobile, - "persist": persist, - "proxy": proxy, - "proxy_country_code": proxy_country_code, - "store_cache": store_cache, - }, - browser_profile_create_params.BrowserProfileCreateParams, - ), - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=BrowserProfileView, - ) - - async def retrieve( - self, - profile_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, - ) -> BrowserProfileView: - """ - Get a specific browser profile by its ID. - - Retrieves the complete details of a browser profile, including all its - configuration settings like viewport dimensions, proxy settings, and behavior - flags. - - Args: - - - profile_id: The unique identifier of the browser profile - - Returns: - - - Complete browser profile information - - Raises: - - - 404: If the user browser profile doesn't exist - - 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 profile_id: - raise ValueError(f"Expected a non-empty value for `profile_id` but received {profile_id!r}") - return await self._get( - f"/browser-profiles/{profile_id}", - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=BrowserProfileView, - ) - - async def update( - self, - profile_id: str, - *, - ad_blocker: Optional[bool] | NotGiven = NOT_GIVEN, - browser_viewport_height: Optional[int] | NotGiven = NOT_GIVEN, - browser_viewport_width: Optional[int] | NotGiven = NOT_GIVEN, - description: Optional[str] | NotGiven = NOT_GIVEN, - is_mobile: Optional[bool] | NotGiven = NOT_GIVEN, - name: Optional[str] | NotGiven = NOT_GIVEN, - persist: Optional[bool] | NotGiven = NOT_GIVEN, - proxy: Optional[bool] | NotGiven = NOT_GIVEN, - proxy_country_code: Optional[ProxyCountryCode] | NotGiven = NOT_GIVEN, - store_cache: Optional[bool] | 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, - ) -> BrowserProfileView: - """ - Update an existing browser profile. - - Modify any aspect of a browser profile, such as its name, description, viewport - settings, or proxy configuration. Only the fields you provide will be updated; - other fields remain unchanged. - - Args: - - - profile_id: The unique identifier of the browser profile to update - - request: The fields to update (only provided fields will be changed) - - Returns: - - - The updated browser profile with all its current details - - Raises: - - - 404: If the user browser profile doesn't exist - - 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 profile_id: - raise ValueError(f"Expected a non-empty value for `profile_id` but received {profile_id!r}") - return await self._patch( - f"/browser-profiles/{profile_id}", - body=await async_maybe_transform( - { - "ad_blocker": ad_blocker, - "browser_viewport_height": browser_viewport_height, - "browser_viewport_width": browser_viewport_width, - "description": description, - "is_mobile": is_mobile, - "name": name, - "persist": persist, - "proxy": proxy, - "proxy_country_code": proxy_country_code, - "store_cache": store_cache, - }, - browser_profile_update_params.BrowserProfileUpdateParams, - ), - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=BrowserProfileView, - ) - - async def list( - self, - *, - page_number: int | NotGiven = NOT_GIVEN, - page_size: 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, - ) -> BrowserProfileListResponse: - """ - Get a paginated list of all browser profiles for the authenticated user. - - Browser profiles define how your web browsers behave during AI agent tasks, - including settings like viewport size, mobile emulation, proxy configuration, - and ad blocking. Use this endpoint to see all your configured browser profiles. - - Returns: - - - A paginated list of browser profiles - - Total count of profiles - - Page information for navigation - - 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( - "/browser-profiles", - options=make_request_options( - extra_headers=extra_headers, - extra_query=extra_query, - extra_body=extra_body, - timeout=timeout, - query=await async_maybe_transform( - { - "page_number": page_number, - "page_size": page_size, - }, - browser_profile_list_params.BrowserProfileListParams, - ), - ), - cast_to=BrowserProfileListResponse, - ) - - async def delete( - self, - profile_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 browser profile. - - Permanently removes a browser profile and all its configuration. This action - cannot be undone. The profile will also be removed from the browser service. Any - active sessions using this profile will continue to work, but you won't be able - to create new sessions with the deleted profile. - - Args: - - - profile_id: The unique identifier of the browser profile to delete - - Returns: - - - 204 No Content on successful deletion (idempotent) - - 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 profile_id: - raise ValueError(f"Expected a non-empty value for `profile_id` but received {profile_id!r}") - extra_headers = {"Accept": "*/*", **(extra_headers or {})} - return await self._delete( - f"/browser-profiles/{profile_id}", - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=NoneType, - ) - - -class BrowserProfilesResourceWithRawResponse: - def __init__(self, browser_profiles: BrowserProfilesResource) -> None: - self._browser_profiles = browser_profiles - - self.create = to_raw_response_wrapper( - browser_profiles.create, - ) - self.retrieve = to_raw_response_wrapper( - browser_profiles.retrieve, - ) - self.update = to_raw_response_wrapper( - browser_profiles.update, - ) - self.list = to_raw_response_wrapper( - browser_profiles.list, - ) - self.delete = to_raw_response_wrapper( - browser_profiles.delete, - ) - - -class AsyncBrowserProfilesResourceWithRawResponse: - def __init__(self, browser_profiles: AsyncBrowserProfilesResource) -> None: - self._browser_profiles = browser_profiles - - self.create = async_to_raw_response_wrapper( - browser_profiles.create, - ) - self.retrieve = async_to_raw_response_wrapper( - browser_profiles.retrieve, - ) - self.update = async_to_raw_response_wrapper( - browser_profiles.update, - ) - self.list = async_to_raw_response_wrapper( - browser_profiles.list, - ) - self.delete = async_to_raw_response_wrapper( - browser_profiles.delete, - ) - - -class BrowserProfilesResourceWithStreamingResponse: - def __init__(self, browser_profiles: BrowserProfilesResource) -> None: - self._browser_profiles = browser_profiles - - self.create = to_streamed_response_wrapper( - browser_profiles.create, - ) - self.retrieve = to_streamed_response_wrapper( - browser_profiles.retrieve, - ) - self.update = to_streamed_response_wrapper( - browser_profiles.update, - ) - self.list = to_streamed_response_wrapper( - browser_profiles.list, - ) - self.delete = to_streamed_response_wrapper( - browser_profiles.delete, - ) - - -class AsyncBrowserProfilesResourceWithStreamingResponse: - def __init__(self, browser_profiles: AsyncBrowserProfilesResource) -> None: - self._browser_profiles = browser_profiles - - self.create = async_to_streamed_response_wrapper( - browser_profiles.create, - ) - self.retrieve = async_to_streamed_response_wrapper( - browser_profiles.retrieve, - ) - self.update = async_to_streamed_response_wrapper( - browser_profiles.update, - ) - self.list = async_to_streamed_response_wrapper( - browser_profiles.list, - ) - self.delete = async_to_streamed_response_wrapper( - browser_profiles.delete, - ) diff --git a/src/browser_use_sdk/resources/sessions/__init__.py b/src/browser_use_sdk/resources/sessions/__init__.py deleted file mode 100644 index fd0ceb3..0000000 --- a/src/browser_use_sdk/resources/sessions/__init__.py +++ /dev/null @@ -1,33 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from .sessions import ( - SessionsResource, - AsyncSessionsResource, - SessionsResourceWithRawResponse, - AsyncSessionsResourceWithRawResponse, - SessionsResourceWithStreamingResponse, - AsyncSessionsResourceWithStreamingResponse, -) -from .public_share import ( - PublicShareResource, - AsyncPublicShareResource, - PublicShareResourceWithRawResponse, - AsyncPublicShareResourceWithRawResponse, - PublicShareResourceWithStreamingResponse, - AsyncPublicShareResourceWithStreamingResponse, -) - -__all__ = [ - "PublicShareResource", - "AsyncPublicShareResource", - "PublicShareResourceWithRawResponse", - "AsyncPublicShareResourceWithRawResponse", - "PublicShareResourceWithStreamingResponse", - "AsyncPublicShareResourceWithStreamingResponse", - "SessionsResource", - "AsyncSessionsResource", - "SessionsResourceWithRawResponse", - "AsyncSessionsResourceWithRawResponse", - "SessionsResourceWithStreamingResponse", - "AsyncSessionsResourceWithStreamingResponse", -] diff --git a/src/browser_use_sdk/resources/sessions/public_share.py b/src/browser_use_sdk/resources/sessions/public_share.py deleted file mode 100644 index 8a235d9..0000000 --- a/src/browser_use_sdk/resources/sessions/public_share.py +++ /dev/null @@ -1,429 +0,0 @@ -# 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, NoneType, 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.share_view import ShareView - -__all__ = ["PublicShareResource", "AsyncPublicShareResource"] - - -class PublicShareResource(SyncAPIResource): - @cached_property - def with_raw_response(self) -> PublicShareResourceWithRawResponse: - """ - 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/browser-use/browser-use-python#accessing-raw-response-data-eg-headers - """ - return PublicShareResourceWithRawResponse(self) - - @cached_property - def with_streaming_response(self) -> PublicShareResourceWithStreamingResponse: - """ - An alternative to `.with_raw_response` that doesn't eagerly read the response body. - - For more information, see https://www.github.com/browser-use/browser-use-python#with_streaming_response - """ - return PublicShareResourceWithStreamingResponse(self) - - def create( - self, - session_id: str, - *, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> ShareView: - """ - Create a public share for a session. - - Generates a public sharing link that allows anyone with the URL to view the - session and its tasks. If a public share already exists for the session, it will - return the existing share instead of creating a new one. - - Public shares are useful for: - - - Sharing results with clients or team members - - Demonstrating AI agent capabilities - - Collaborative review of automated tasks - - Args: - - - session_id: The unique identifier of the agent session to share - - Returns: - - - Public share information including the share URL and usage statistics - - Raises: - - - 404: If the user agent session doesn't exist - - Args: - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - if not session_id: - raise ValueError(f"Expected a non-empty value for `session_id` but received {session_id!r}") - return self._post( - f"/sessions/{session_id}/public-share", - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=ShareView, - ) - - def retrieve( - self, - session_id: str, - *, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> ShareView: - """ - Get information about the public share for a session. - - Retrieves details about the public sharing link for a session, including the - share token, public URL, view count, and last viewed timestamp. This is useful - for monitoring how your shared sessions are being accessed. - - Args: - - - session_id: The unique identifier of the agent session - - Returns: - - - Public share information including the share URL and usage statistics - - Raises: - - - 404: If the user agent session doesn't exist or doesn't have a public share - - Args: - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - if not session_id: - raise ValueError(f"Expected a non-empty value for `session_id` but received {session_id!r}") - return self._get( - f"/sessions/{session_id}/public-share", - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=ShareView, - ) - - def delete( - self, - session_id: str, - *, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> None: - """ - Remove the public share for a session. - - Deletes the public sharing link for a session, making it no longer accessible to - anyone with the previous share URL. This is useful for removing access to - sensitive sessions or when you no longer want to share the results. - - Args: - - - session_id: The unique identifier of the agent session - - Returns: - - - 204 No Content on successful deletion (idempotent) - - Raises: - - - 404: If the user agent session doesn't exist - - Args: - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - if not session_id: - raise ValueError(f"Expected a non-empty value for `session_id` but received {session_id!r}") - extra_headers = {"Accept": "*/*", **(extra_headers or {})} - return self._delete( - f"/sessions/{session_id}/public-share", - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=NoneType, - ) - - -class AsyncPublicShareResource(AsyncAPIResource): - @cached_property - def with_raw_response(self) -> AsyncPublicShareResourceWithRawResponse: - """ - 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/browser-use/browser-use-python#accessing-raw-response-data-eg-headers - """ - return AsyncPublicShareResourceWithRawResponse(self) - - @cached_property - def with_streaming_response(self) -> AsyncPublicShareResourceWithStreamingResponse: - """ - An alternative to `.with_raw_response` that doesn't eagerly read the response body. - - For more information, see https://www.github.com/browser-use/browser-use-python#with_streaming_response - """ - return AsyncPublicShareResourceWithStreamingResponse(self) - - async def create( - self, - session_id: str, - *, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> ShareView: - """ - Create a public share for a session. - - Generates a public sharing link that allows anyone with the URL to view the - session and its tasks. If a public share already exists for the session, it will - return the existing share instead of creating a new one. - - Public shares are useful for: - - - Sharing results with clients or team members - - Demonstrating AI agent capabilities - - Collaborative review of automated tasks - - Args: - - - session_id: The unique identifier of the agent session to share - - Returns: - - - Public share information including the share URL and usage statistics - - Raises: - - - 404: If the user agent session doesn't exist - - Args: - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - if not session_id: - raise ValueError(f"Expected a non-empty value for `session_id` but received {session_id!r}") - return await self._post( - f"/sessions/{session_id}/public-share", - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=ShareView, - ) - - async def retrieve( - self, - session_id: str, - *, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> ShareView: - """ - Get information about the public share for a session. - - Retrieves details about the public sharing link for a session, including the - share token, public URL, view count, and last viewed timestamp. This is useful - for monitoring how your shared sessions are being accessed. - - Args: - - - session_id: The unique identifier of the agent session - - Returns: - - - Public share information including the share URL and usage statistics - - Raises: - - - 404: If the user agent session doesn't exist or doesn't have a public share - - Args: - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - if not session_id: - raise ValueError(f"Expected a non-empty value for `session_id` but received {session_id!r}") - return await self._get( - f"/sessions/{session_id}/public-share", - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=ShareView, - ) - - async def delete( - self, - session_id: str, - *, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> None: - """ - Remove the public share for a session. - - Deletes the public sharing link for a session, making it no longer accessible to - anyone with the previous share URL. This is useful for removing access to - sensitive sessions or when you no longer want to share the results. - - Args: - - - session_id: The unique identifier of the agent session - - Returns: - - - 204 No Content on successful deletion (idempotent) - - Raises: - - - 404: If the user agent session doesn't exist - - Args: - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - if not session_id: - raise ValueError(f"Expected a non-empty value for `session_id` but received {session_id!r}") - extra_headers = {"Accept": "*/*", **(extra_headers or {})} - return await self._delete( - f"/sessions/{session_id}/public-share", - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=NoneType, - ) - - -class PublicShareResourceWithRawResponse: - def __init__(self, public_share: PublicShareResource) -> None: - self._public_share = public_share - - self.create = to_raw_response_wrapper( - public_share.create, - ) - self.retrieve = to_raw_response_wrapper( - public_share.retrieve, - ) - self.delete = to_raw_response_wrapper( - public_share.delete, - ) - - -class AsyncPublicShareResourceWithRawResponse: - def __init__(self, public_share: AsyncPublicShareResource) -> None: - self._public_share = public_share - - self.create = async_to_raw_response_wrapper( - public_share.create, - ) - self.retrieve = async_to_raw_response_wrapper( - public_share.retrieve, - ) - self.delete = async_to_raw_response_wrapper( - public_share.delete, - ) - - -class PublicShareResourceWithStreamingResponse: - def __init__(self, public_share: PublicShareResource) -> None: - self._public_share = public_share - - self.create = to_streamed_response_wrapper( - public_share.create, - ) - self.retrieve = to_streamed_response_wrapper( - public_share.retrieve, - ) - self.delete = to_streamed_response_wrapper( - public_share.delete, - ) - - -class AsyncPublicShareResourceWithStreamingResponse: - def __init__(self, public_share: AsyncPublicShareResource) -> None: - self._public_share = public_share - - self.create = async_to_streamed_response_wrapper( - public_share.create, - ) - self.retrieve = async_to_streamed_response_wrapper( - public_share.retrieve, - ) - self.delete = async_to_streamed_response_wrapper( - public_share.delete, - ) diff --git a/src/browser_use_sdk/resources/sessions/sessions.py b/src/browser_use_sdk/resources/sessions/sessions.py deleted file mode 100644 index 4fc67dd..0000000 --- a/src/browser_use_sdk/resources/sessions/sessions.py +++ /dev/null @@ -1,618 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -from typing import Optional -from typing_extensions import Literal - -import httpx - -from ...types import SessionStatus, session_list_params, session_update_params -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 -from ..._response import ( - to_raw_response_wrapper, - to_streamed_response_wrapper, - async_to_raw_response_wrapper, - async_to_streamed_response_wrapper, -) -from .public_share import ( - PublicShareResource, - AsyncPublicShareResource, - PublicShareResourceWithRawResponse, - AsyncPublicShareResourceWithRawResponse, - PublicShareResourceWithStreamingResponse, - AsyncPublicShareResourceWithStreamingResponse, -) -from ..._base_client import make_request_options -from ...types.session_view import SessionView -from ...types.session_status import SessionStatus -from ...types.session_list_response import SessionListResponse - -__all__ = ["SessionsResource", "AsyncSessionsResource"] - - -class SessionsResource(SyncAPIResource): - @cached_property - def public_share(self) -> PublicShareResource: - return PublicShareResource(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 raw response object instead of the parsed content. - - For more information, see https://www.github.com/browser-use/browser-use-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/browser-use/browser-use-python#with_streaming_response - """ - return SessionsResourceWithStreamingResponse(self) - - def retrieve( - self, - session_id: str, - *, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> SessionView: - """ - Get detailed information about a specific AI agent session. - - Retrieves comprehensive information about a session, including its current - status, live browser URL (if active), recording URL (if completed), and optional - task details. This endpoint is useful for monitoring active sessions or - reviewing completed ones. - - Args: - - - session_id: The unique identifier of the agent session - - params: Optional parameters to control what data is included - - Returns: - - - Complete session information including status, URLs, and optional task details - - Raises: - - - 404: If the user agent session doesn't exist - - Args: - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - if not session_id: - raise ValueError(f"Expected a non-empty value for `session_id` but received {session_id!r}") - return self._get( - f"/sessions/{session_id}", - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=SessionView, - ) - - def update( - self, - session_id: str, - *, - action: Literal["stop"], - # 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, - ) -> SessionView: - """ - Update a session's status or perform actions on it. - - Currently supports stopping a session, which will: - - 1. Stop any running tasks in the session - 2. End the browser session - 3. Generate a recording URL if available - 4. Update the session status to 'stopped' - - This is useful for manually stopping long-running sessions or when you want to - end a session before all tasks are complete. - - Args: - - - session_id: The unique identifier of the agent session to update - - request: The action to perform on the session - - Returns: - - - The updated session information including the new status and recording URL - - Raises: - - - 404: If the user agent session doesn't exist - - Args: - action: Available actions that can be performed on a session - - Attributes: STOP: Stop the session and all its associated tasks (cannot be - undone) - - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - if not session_id: - raise ValueError(f"Expected a non-empty value for `session_id` but received {session_id!r}") - return self._patch( - f"/sessions/{session_id}", - body=maybe_transform({"action": action}, session_update_params.SessionUpdateParams), - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=SessionView, - ) - - def list( - self, - *, - filter_by: Optional[SessionStatus] | NotGiven = NOT_GIVEN, - page_number: int | NotGiven = NOT_GIVEN, - page_size: 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, - ) -> SessionListResponse: - """ - Get a paginated list of all AI agent sessions for the authenticated user. - - AI agent sessions represent active or completed browsing sessions where your AI - agents perform tasks. Each session can contain multiple tasks and maintains - browser state throughout the session lifecycle. - - You can filter sessions by status and optionally include task details for each - session. - - Returns: - - - A paginated list of agent sessions - - Total count of sessions - - Page information for navigation - - Optional task details for each session (if requested) - - Args: - filter_by: Enumeration of possible (browser) session states - - Attributes: ACTIVE: Session is currently active and running (browser is running) - STOPPED: Session has been stopped and is no longer active (browser is stopped) - - 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( - "/sessions", - options=make_request_options( - extra_headers=extra_headers, - extra_query=extra_query, - extra_body=extra_body, - timeout=timeout, - query=maybe_transform( - { - "filter_by": filter_by, - "page_number": page_number, - "page_size": page_size, - }, - session_list_params.SessionListParams, - ), - ), - cast_to=SessionListResponse, - ) - - def delete( - self, - session_id: str, - *, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> None: - """ - Delete a session and all its associated data. - - Permanently removes a session and all its tasks, browser data, and public - shares. This action cannot be undone. Use this endpoint to clean up old sessions - and free up storage space. - - Args: - - - session_id: The unique identifier of the agent session to delete - - Returns: - - - 204 No Content on successful deletion (idempotent) - - Args: - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - if not session_id: - raise ValueError(f"Expected a non-empty value for `session_id` but received {session_id!r}") - extra_headers = {"Accept": "*/*", **(extra_headers or {})} - return self._delete( - f"/sessions/{session_id}", - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=NoneType, - ) - - -class AsyncSessionsResource(AsyncAPIResource): - @cached_property - def public_share(self) -> AsyncPublicShareResource: - return AsyncPublicShareResource(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 raw response object instead of the parsed content. - - For more information, see https://www.github.com/browser-use/browser-use-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/browser-use/browser-use-python#with_streaming_response - """ - return AsyncSessionsResourceWithStreamingResponse(self) - - async def retrieve( - self, - session_id: str, - *, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> SessionView: - """ - Get detailed information about a specific AI agent session. - - Retrieves comprehensive information about a session, including its current - status, live browser URL (if active), recording URL (if completed), and optional - task details. This endpoint is useful for monitoring active sessions or - reviewing completed ones. - - Args: - - - session_id: The unique identifier of the agent session - - params: Optional parameters to control what data is included - - Returns: - - - Complete session information including status, URLs, and optional task details - - Raises: - - - 404: If the user agent session doesn't exist - - Args: - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - if not session_id: - raise ValueError(f"Expected a non-empty value for `session_id` but received {session_id!r}") - return await self._get( - f"/sessions/{session_id}", - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=SessionView, - ) - - async def update( - self, - session_id: str, - *, - action: Literal["stop"], - # 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, - ) -> SessionView: - """ - Update a session's status or perform actions on it. - - Currently supports stopping a session, which will: - - 1. Stop any running tasks in the session - 2. End the browser session - 3. Generate a recording URL if available - 4. Update the session status to 'stopped' - - This is useful for manually stopping long-running sessions or when you want to - end a session before all tasks are complete. - - Args: - - - session_id: The unique identifier of the agent session to update - - request: The action to perform on the session - - Returns: - - - The updated session information including the new status and recording URL - - Raises: - - - 404: If the user agent session doesn't exist - - Args: - action: Available actions that can be performed on a session - - Attributes: STOP: Stop the session and all its associated tasks (cannot be - undone) - - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - if not session_id: - raise ValueError(f"Expected a non-empty value for `session_id` but received {session_id!r}") - return await self._patch( - f"/sessions/{session_id}", - body=await async_maybe_transform({"action": action}, session_update_params.SessionUpdateParams), - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=SessionView, - ) - - async def list( - self, - *, - filter_by: Optional[SessionStatus] | NotGiven = NOT_GIVEN, - page_number: int | NotGiven = NOT_GIVEN, - page_size: 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, - ) -> SessionListResponse: - """ - Get a paginated list of all AI agent sessions for the authenticated user. - - AI agent sessions represent active or completed browsing sessions where your AI - agents perform tasks. Each session can contain multiple tasks and maintains - browser state throughout the session lifecycle. - - You can filter sessions by status and optionally include task details for each - session. - - Returns: - - - A paginated list of agent sessions - - Total count of sessions - - Page information for navigation - - Optional task details for each session (if requested) - - Args: - filter_by: Enumeration of possible (browser) session states - - Attributes: ACTIVE: Session is currently active and running (browser is running) - STOPPED: Session has been stopped and is no longer active (browser is stopped) - - 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( - "/sessions", - options=make_request_options( - extra_headers=extra_headers, - extra_query=extra_query, - extra_body=extra_body, - timeout=timeout, - query=await async_maybe_transform( - { - "filter_by": filter_by, - "page_number": page_number, - "page_size": page_size, - }, - session_list_params.SessionListParams, - ), - ), - cast_to=SessionListResponse, - ) - - async def delete( - self, - session_id: str, - *, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> None: - """ - Delete a session and all its associated data. - - Permanently removes a session and all its tasks, browser data, and public - shares. This action cannot be undone. Use this endpoint to clean up old sessions - and free up storage space. - - Args: - - - session_id: The unique identifier of the agent session to delete - - Returns: - - - 204 No Content on successful deletion (idempotent) - - Args: - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - if not session_id: - raise ValueError(f"Expected a non-empty value for `session_id` but received {session_id!r}") - extra_headers = {"Accept": "*/*", **(extra_headers or {})} - return await self._delete( - f"/sessions/{session_id}", - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=NoneType, - ) - - -class SessionsResourceWithRawResponse: - def __init__(self, sessions: SessionsResource) -> None: - self._sessions = sessions - - 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.delete = to_raw_response_wrapper( - sessions.delete, - ) - - @cached_property - def public_share(self) -> PublicShareResourceWithRawResponse: - return PublicShareResourceWithRawResponse(self._sessions.public_share) - - -class AsyncSessionsResourceWithRawResponse: - def __init__(self, sessions: AsyncSessionsResource) -> None: - self._sessions = sessions - - 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.delete = async_to_raw_response_wrapper( - sessions.delete, - ) - - @cached_property - def public_share(self) -> AsyncPublicShareResourceWithRawResponse: - return AsyncPublicShareResourceWithRawResponse(self._sessions.public_share) - - -class SessionsResourceWithStreamingResponse: - def __init__(self, sessions: SessionsResource) -> None: - self._sessions = sessions - - 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.delete = to_streamed_response_wrapper( - sessions.delete, - ) - - @cached_property - def public_share(self) -> PublicShareResourceWithStreamingResponse: - return PublicShareResourceWithStreamingResponse(self._sessions.public_share) - - -class AsyncSessionsResourceWithStreamingResponse: - def __init__(self, sessions: AsyncSessionsResource) -> None: - self._sessions = sessions - - 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.delete = async_to_streamed_response_wrapper( - sessions.delete, - ) - - @cached_property - def public_share(self) -> AsyncPublicShareResourceWithStreamingResponse: - return AsyncPublicShareResourceWithStreamingResponse(self._sessions.public_share) diff --git a/src/browser_use_sdk/resources/tasks.py b/src/browser_use_sdk/resources/tasks.py deleted file mode 100644 index 8e96d22..0000000 --- a/src/browser_use_sdk/resources/tasks.py +++ /dev/null @@ -1,1741 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -import json -import time -import asyncio -from typing import Dict, List, Union, TypeVar, Iterator, Optional, AsyncIterator, overload -from datetime import datetime -from typing_extensions import Literal - -import httpx -from pydantic import BaseModel - -from ..types import TaskStatus, task_list_params, task_create_params, task_update_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 ..lib.parse import TaskViewWithOutput, hash_task_view -from .._base_client import make_request_options -from ..types.task_view import TaskView -from ..types.task_status import TaskStatus -from ..types.task_list_response import TaskListResponse -from ..types.task_create_response import TaskCreateResponse -from ..types.task_get_logs_response import TaskGetLogsResponse -from ..types.task_get_output_file_response import TaskGetOutputFileResponse -from ..types.task_get_user_uploaded_file_response import TaskGetUserUploadedFileResponse - -__all__ = ["TasksResource", "AsyncTasksResource"] - -T = TypeVar("T", bound=BaseModel) - - -class TasksResource(SyncAPIResource): - @cached_property - def with_raw_response(self) -> TasksResourceWithRawResponse: - """ - 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/browser-use/browser-use-python#accessing-raw-response-data-eg-headers - """ - return TasksResourceWithRawResponse(self) - - @cached_property - def with_streaming_response(self) -> TasksResourceWithStreamingResponse: - """ - An alternative to `.with_raw_response` that doesn't eagerly read the response body. - - For more information, see https://www.github.com/browser-use/browser-use-python#with_streaming_response - """ - return TasksResourceWithStreamingResponse(self) - - @overload - def run( - self, - *, - task: str, - agent_settings: task_create_params.AgentSettings | NotGiven = NOT_GIVEN, - browser_settings: task_create_params.BrowserSettings | NotGiven = NOT_GIVEN, - included_file_names: Optional[List[str]] | NotGiven = NOT_GIVEN, - metadata: Optional[Dict[str, str]] | NotGiven = NOT_GIVEN, - secrets: Optional[Dict[str, str]] | NotGiven = NOT_GIVEN, - structured_output_json: Optional[str] | 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, - ) -> TaskView: ... - - @overload - def run( - self, - *, - task: str, - agent_settings: task_create_params.AgentSettings | NotGiven = NOT_GIVEN, - browser_settings: task_create_params.BrowserSettings | NotGiven = NOT_GIVEN, - included_file_names: Optional[List[str]] | NotGiven = NOT_GIVEN, - metadata: Optional[Dict[str, str]] | NotGiven = NOT_GIVEN, - secrets: Optional[Dict[str, str]] | NotGiven = NOT_GIVEN, - structured_output_json: type[T], - # 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, - ) -> TaskViewWithOutput[T]: ... - - def run( - self, - *, - task: str, - agent_settings: task_create_params.AgentSettings | NotGiven = NOT_GIVEN, - browser_settings: task_create_params.BrowserSettings | NotGiven = NOT_GIVEN, - included_file_names: Optional[List[str]] | NotGiven = NOT_GIVEN, - metadata: Optional[Dict[str, str]] | NotGiven = NOT_GIVEN, - secrets: Optional[Dict[str, str]] | NotGiven = NOT_GIVEN, - structured_output_json: Optional[Union[type[BaseModel], str]] | 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, - ) -> Union[TaskView, TaskViewWithOutput[BaseModel]]: - """ - Run a new task and return the task view. - """ - if structured_output_json is not None and isinstance(structured_output_json, type): - create_task_res = self.create( - task=task, - agent_settings=agent_settings, - browser_settings=browser_settings, - included_file_names=included_file_names, - metadata=metadata, - secrets=secrets, - structured_output_json=structured_output_json, - extra_headers=extra_headers, - extra_query=extra_query, - extra_body=extra_body, - timeout=timeout, - ) - - for structured_msg in self.stream(create_task_res.id, structured_output_json=structured_output_json): - if structured_msg.status == "finished": - return structured_msg - - raise ValueError("Task did not finish") - - else: - create_task_res = self.create( - task=task, - agent_settings=agent_settings, - browser_settings=browser_settings, - included_file_names=included_file_names, - metadata=metadata, - secrets=secrets, - structured_output_json=structured_output_json, - extra_headers=extra_headers, - extra_query=extra_query, - extra_body=extra_body, - timeout=timeout, - ) - - for msg in self.stream(create_task_res.id): - if msg.status == "finished": - return msg - - raise ValueError("Task did not finish") - - @overload - def create( - self, - *, - task: str, - agent_settings: task_create_params.AgentSettings | NotGiven = NOT_GIVEN, - browser_settings: task_create_params.BrowserSettings | NotGiven = NOT_GIVEN, - included_file_names: Optional[List[str]] | NotGiven = NOT_GIVEN, - metadata: Optional[Dict[str, str]] | NotGiven = NOT_GIVEN, - secrets: Optional[Dict[str, str]] | NotGiven = NOT_GIVEN, - structured_output_json: Optional[str] | 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, - ) -> TaskCreateResponse: ... - - @overload - def create( - self, - *, - task: str, - agent_settings: task_create_params.AgentSettings | NotGiven = NOT_GIVEN, - browser_settings: task_create_params.BrowserSettings | NotGiven = NOT_GIVEN, - included_file_names: Optional[List[str]] | NotGiven = NOT_GIVEN, - metadata: Optional[Dict[str, str]] | NotGiven = NOT_GIVEN, - secrets: Optional[Dict[str, str]] | NotGiven = NOT_GIVEN, - structured_output_json: type[BaseModel], - # 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, - ) -> TaskCreateResponse: ... - - def create( - self, - *, - task: str, - agent_settings: task_create_params.AgentSettings | NotGiven = NOT_GIVEN, - browser_settings: task_create_params.BrowserSettings | NotGiven = NOT_GIVEN, - included_file_names: Optional[List[str]] | NotGiven = NOT_GIVEN, - metadata: Optional[Dict[str, str]] | NotGiven = NOT_GIVEN, - secrets: Optional[Dict[str, str]] | NotGiven = NOT_GIVEN, - structured_output_json: Optional[Union[type[BaseModel], str]] | 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, - ) -> TaskCreateResponse: - """ - Create and start a new Browser Use Agent task. - - This is the main endpoint for running AI agents. You can either: - - 1. Start a new session with a new task. - 2. Add a follow-up task to an existing session. - - When starting a new session: - - - A new browser session is created - - Credits are deducted from your account - - The agent begins executing your task immediately - - When adding to an existing session: - - - The agent continues in the same browser context - - No additional browser start up costs are charged (browser session is already - active) - - The agent can build on previous work - - Key features: - - - Agent profiles: Define agent behavior and capabilities - - Browser profiles: Control browser settings and environment (only used for new - sessions) - - File uploads: Include documents for the agent to work with - - Structured output: Define the format of the task result - - Task metadata: Add custom data for tracking and organization - - Args: - - - request: Complete task configuration including agent settings, browser - settings, and task description - - Returns: - - - The created task ID together with the task's session ID - - Raises: - - - 402: If user has insufficient credits for a new session - - 404: If referenced agent/browser profiles don't exist - - 400: If session is stopped or already has a running task - - Args: - agent_settings: Configuration settings for the agent - - Attributes: llm: The LLM model to use for the agent start_url: Optional URL to - start the agent on (will not be changed as a step) profile_id: Unique identifier - of the agent profile to use for the task - - browser_settings: Configuration settings for the browser session - - Attributes: session_id: Unique identifier of existing session to continue - profile_id: Unique identifier of browser profile to use (use if you want to - start a new session) - - 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 ( - structured_output_json is not None - and not isinstance(structured_output_json, str) - and isinstance(structured_output_json, type) - ): - structured_output_json = json.dumps(structured_output_json.model_json_schema()) - - return self._post( - "/tasks", - body=maybe_transform( - { - "task": task, - "agent_settings": agent_settings, - "browser_settings": browser_settings, - "included_file_names": included_file_names, - "metadata": metadata, - "secrets": secrets, - "structured_output_json": structured_output_json, - }, - task_create_params.TaskCreateParams, - ), - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=TaskCreateResponse, - ) - - @overload - def retrieve( - self, - task_id: str, - structured_output_json: type[T], - *, - # 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, - ) -> TaskViewWithOutput[T]: ... - - @overload - def retrieve( - self, - task_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, - ) -> TaskView: ... - - def retrieve( - self, - task_id: str, - structured_output_json: Optional[type[BaseModel]] | 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, - ) -> Union[TaskView, TaskViewWithOutput[BaseModel]]: - """ - Get detailed information about a specific AI agent task. - - Retrieves comprehensive information about a task, including its current status, - progress, and detailed execution data. You can choose to get just the status - (for quick polling) or full details including steps and file information. - - Use this endpoint to: - - - Monitor task progress in real-time - - Review completed task results - - Debug failed tasks by examining steps - - Download output files and logs - - Args: - - - task_id: The unique identifier of the agent task - - Returns: - - - Complete task information - - Raises: - - - 404: If the user agent task doesn't exist - - 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 task_id: - raise ValueError(f"Expected a non-empty value for `task_id` but received {task_id!r}") - - if structured_output_json is not None and isinstance(structured_output_json, type): - res = self._get( - f"/tasks/{task_id}", - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=TaskView, - ) - - if res.done_output is None: - return TaskViewWithOutput[BaseModel]( - **res.model_dump(), - parsed_output=None, - ) - - parsed_output = structured_output_json.model_validate_json(res.done_output) - - return TaskViewWithOutput[BaseModel]( - **res.model_dump(), - parsed_output=parsed_output, - ) - - return self._get( - f"/tasks/{task_id}", - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=TaskView, - ) - - @overload - def stream( - self, - task_id: str, - structured_output_json: type[T], - *, - # 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, - ) -> Iterator[TaskViewWithOutput[T]]: ... - - @overload - def stream( - self, - task_id: str, - structured_output_json: None | 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, - ) -> Iterator[TaskView]: ... - - def stream( - self, - task_id: str, - structured_output_json: type[T] | None | 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, - ) -> Iterator[TaskView | TaskViewWithOutput[T]]: - """ - Stream the task view as it is updated until the task is finished. - """ - - for res in self._watch( - task_id=task_id, - extra_headers=extra_headers, - extra_query=extra_query, - extra_body=extra_body, - timeout=timeout, - ): - if structured_output_json is not None and isinstance(structured_output_json, type): - if res.done_output is None: - yield TaskViewWithOutput[T]( - **res.model_dump(), - parsed_output=None, - ) - else: - schema: type[T] = structured_output_json - parsed_output: T = schema.model_validate_json(res.done_output) - - yield TaskViewWithOutput[T]( - **res.model_dump(), - parsed_output=parsed_output, - ) - - else: - yield res - - def _watch( - self, - task_id: str, - interval: float = 1, - *, - # 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, - ) -> Iterator[TaskView]: - """Converts a polling loop into a generator loop.""" - hash: str | None = None - - while True: - res = self.retrieve( - task_id=task_id, - extra_headers=extra_headers, - extra_query=extra_query, - extra_body=extra_body, - timeout=timeout, - ) - - res_hash = hash_task_view(res) - - if hash is None or res_hash != hash: - hash = res_hash - yield res - - if res.status == "finished": - break - - time.sleep(interval) - - def update( - self, - task_id: str, - *, - action: Literal["stop", "pause", "resume", "stop_task_and_session"], - # 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, - ) -> TaskView: - """ - Control the execution of an AI agent task. - - Allows you to pause, resume, or stop tasks, and optionally stop the entire - session. This is useful for: - - - Pausing long-running tasks to review progress - - Stopping tasks that are taking too long - - Ending sessions when you're done with all tasks - - Available actions: - - - STOP: Stop the current task - - PAUSE: Pause the task (can be resumed later) - - RESUME: Resume a paused task - - STOP_TASK_AND_SESSION: Stop the task and end the entire session - - Args: - - - task_id: The unique identifier of the agent task to control - - request: The action to perform on the task - - Returns: - - - The updated task information - - Raises: - - - 404: If the user agent task doesn't exist - - Args: - action: Available actions that can be performed on a task - - Attributes: STOP: Stop the current task execution PAUSE: Pause the current task - execution RESUME: Resume a paused task execution STOP_TASK_AND_SESSION: Stop - both the task and its parent session - - 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 task_id: - raise ValueError(f"Expected a non-empty value for `task_id` but received {task_id!r}") - return self._patch( - f"/tasks/{task_id}", - body=maybe_transform({"action": action}, task_update_params.TaskUpdateParams), - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=TaskView, - ) - - def list( - self, - *, - after: Union[str, datetime, None] | NotGiven = NOT_GIVEN, - before: Union[str, datetime, None] | NotGiven = NOT_GIVEN, - filter_by: Optional[TaskStatus] | NotGiven = NOT_GIVEN, - page_number: int | NotGiven = NOT_GIVEN, - page_size: int | NotGiven = NOT_GIVEN, - session_id: Optional[str] | 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, - ) -> TaskListResponse: - """ - Get a paginated list of all Browser Use Agent tasks for the authenticated user. - - Browser Use Agent tasks are the individual jobs that your agents perform within - a session. Each task represents a specific instruction or goal that the agent - works on, such as filling out a form, extracting data, or navigating to specific - pages. - - Returns: - - - A paginated list of Browser Use Agent tasks - - Total count of Browser Use Agent tasks - - Page information for navigation - - Args: - filter_by: Enumeration of possible task execution states - - Attributes: STARTED: Task has been started and is currently running. PAUSED: - Task execution has been temporarily paused (can be resumed) FINISHED: Task has - finished and the agent has completed the task. STOPPED: Task execution has been - manually stopped (cannot be resumed). - - 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( - "/tasks", - options=make_request_options( - extra_headers=extra_headers, - extra_query=extra_query, - extra_body=extra_body, - timeout=timeout, - query=maybe_transform( - { - "after": after, - "before": before, - "filter_by": filter_by, - "page_number": page_number, - "page_size": page_size, - "session_id": session_id, - }, - task_list_params.TaskListParams, - ), - ), - cast_to=TaskListResponse, - ) - - def get_logs( - self, - task_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, - ) -> TaskGetLogsResponse: - """ - Get a download URL for the execution logs of an AI agent task. - - Task logs contain detailed information about how the AI agent executed the task, - including: - - - Step-by-step reasoning and decisions - - Actions taken on web pages - - Error messages and debugging information - - Performance metrics and timing data - - This is useful for: - - - Understanding how the agent solved the task - - Debugging failed or unexpected results - - Optimizing agent behavior and prompts - - Auditing agent actions for compliance - - Args: - - - task_id: The unique identifier of the agent task - - Returns: - - - A presigned download URL for the task log file - - Raises: - - - 404: If the user agent task doesn't exist - - 500: If the download URL cannot be generated (should not happen) - - 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 task_id: - raise ValueError(f"Expected a non-empty value for `task_id` but received {task_id!r}") - return self._get( - f"/tasks/{task_id}/logs", - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=TaskGetLogsResponse, - ) - - def get_output_file( - self, - file_id: str, - *, - task_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, - ) -> TaskGetOutputFileResponse: - """ - Get a download URL for a specific output file generated by an AI agent task. - - AI agents can generate various output files during task execution, such as: - - - Screenshots of web pages - - Extracted data in CSV/JSON format - - Generated reports or documents - - Downloaded files from websites - - This endpoint provides a secure, time-limited download URL for accessing these - files. The URL expires after a short time for security. - - Args: - - - task_id: The unique identifier of the agent task - - file_id: The unique identifier of the output file - - Returns: - - - A presigned download URL for the requested file - - Raises: - - - 404: If the user agent task or output file doesn't exist - - 500: If the download URL cannot be generated (should not happen) - - 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 task_id: - raise ValueError(f"Expected a non-empty value for `task_id` but received {task_id!r}") - if not file_id: - raise ValueError(f"Expected a non-empty value for `file_id` but received {file_id!r}") - return self._get( - f"/tasks/{task_id}/output-files/{file_id}", - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=TaskGetOutputFileResponse, - ) - - def get_user_uploaded_file( - self, - file_id: str, - *, - task_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, - ) -> TaskGetUserUploadedFileResponse: - """ - Get a download URL for a specific user uploaded file that was used in the task. - - A user can upload files to their account file bucket and reference the name of - the file in a task. These files are then made available for the agent to use - during the agent task run. - - This endpoint provides a secure, time-limited download URL for accessing these - files. The URL expires after a short time for security. - - Args: - - - task_id: The unique identifier of the agent task - - file_id: The unique identifier of the user uploaded file - - Returns: - - - A presigned download URL for the requested file - - Raises: - - - 404: If the user agent task or user uploaded file doesn't exist - - 500: If the download URL cannot be generated (should not happen) - - 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 task_id: - raise ValueError(f"Expected a non-empty value for `task_id` but received {task_id!r}") - if not file_id: - raise ValueError(f"Expected a non-empty value for `file_id` but received {file_id!r}") - return self._get( - f"/tasks/{task_id}/user-uploaded-files/{file_id}", - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=TaskGetUserUploadedFileResponse, - ) - - -class AsyncTasksResource(AsyncAPIResource): - @cached_property - def with_raw_response(self) -> AsyncTasksResourceWithRawResponse: - """ - 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/browser-use/browser-use-python#accessing-raw-response-data-eg-headers - """ - return AsyncTasksResourceWithRawResponse(self) - - @cached_property - def with_streaming_response(self) -> AsyncTasksResourceWithStreamingResponse: - """ - An alternative to `.with_raw_response` that doesn't eagerly read the response body. - - For more information, see https://www.github.com/browser-use/browser-use-python#with_streaming_response - """ - return AsyncTasksResourceWithStreamingResponse(self) - - @overload - async def run( - self, - *, - task: str, - agent_settings: task_create_params.AgentSettings | NotGiven = NOT_GIVEN, - browser_settings: task_create_params.BrowserSettings | NotGiven = NOT_GIVEN, - included_file_names: Optional[List[str]] | NotGiven = NOT_GIVEN, - metadata: Optional[Dict[str, str]] | NotGiven = NOT_GIVEN, - secrets: Optional[Dict[str, str]] | NotGiven = NOT_GIVEN, - structured_output_json: Optional[str] | 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, - ) -> TaskView: ... - - @overload - async def run( - self, - *, - task: str, - agent_settings: task_create_params.AgentSettings | NotGiven = NOT_GIVEN, - browser_settings: task_create_params.BrowserSettings | NotGiven = NOT_GIVEN, - included_file_names: Optional[List[str]] | NotGiven = NOT_GIVEN, - metadata: Optional[Dict[str, str]] | NotGiven = NOT_GIVEN, - secrets: Optional[Dict[str, str]] | NotGiven = NOT_GIVEN, - structured_output_json: type[T], - # 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, - ) -> TaskViewWithOutput[T]: ... - - async def run( - self, - *, - task: str, - agent_settings: task_create_params.AgentSettings | NotGiven = NOT_GIVEN, - browser_settings: task_create_params.BrowserSettings | NotGiven = NOT_GIVEN, - included_file_names: Optional[List[str]] | NotGiven = NOT_GIVEN, - metadata: Optional[Dict[str, str]] | NotGiven = NOT_GIVEN, - secrets: Optional[Dict[str, str]] | NotGiven = NOT_GIVEN, - structured_output_json: Optional[Union[type[BaseModel], str]] | 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, - ) -> Union[TaskView, TaskViewWithOutput[BaseModel]]: - """ - Run a new Browser Use Agent task. - """ - if structured_output_json is not None and isinstance(structured_output_json, type): - create_task_res = await self.create( - task=task, - agent_settings=agent_settings, - browser_settings=browser_settings, - included_file_names=included_file_names, - metadata=metadata, - secrets=secrets, - structured_output_json=structured_output_json, - extra_headers=extra_headers, - extra_query=extra_query, - extra_body=extra_body, - timeout=timeout, - ) - - async for structured_msg in self.stream(create_task_res.id, structured_output_json=structured_output_json): - if structured_msg.status == "finished": - return structured_msg - - raise ValueError("Task did not finish") - - else: - create_task_res = await self.create( - task=task, - agent_settings=agent_settings, - browser_settings=browser_settings, - included_file_names=included_file_names, - metadata=metadata, - secrets=secrets, - structured_output_json=structured_output_json, - extra_headers=extra_headers, - extra_query=extra_query, - extra_body=extra_body, - timeout=timeout, - ) - - async for msg in self.stream(create_task_res.id): - if msg.status == "finished": - return msg - - raise ValueError("Task did not finish") - - @overload - async def create( - self, - *, - task: str, - agent_settings: task_create_params.AgentSettings | NotGiven = NOT_GIVEN, - browser_settings: task_create_params.BrowserSettings | NotGiven = NOT_GIVEN, - included_file_names: Optional[List[str]] | NotGiven = NOT_GIVEN, - metadata: Optional[Dict[str, str]] | NotGiven = NOT_GIVEN, - secrets: Optional[Dict[str, str]] | NotGiven = NOT_GIVEN, - structured_output_json: Optional[str] | 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, - ) -> TaskCreateResponse: ... - - @overload - async def create( - self, - *, - task: str, - agent_settings: task_create_params.AgentSettings | NotGiven = NOT_GIVEN, - browser_settings: task_create_params.BrowserSettings | NotGiven = NOT_GIVEN, - included_file_names: Optional[List[str]] | NotGiven = NOT_GIVEN, - metadata: Optional[Dict[str, str]] | NotGiven = NOT_GIVEN, - secrets: Optional[Dict[str, str]] | NotGiven = NOT_GIVEN, - structured_output_json: type[BaseModel], - # 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, - ) -> TaskCreateResponse: ... - - async def create( - self, - *, - task: str, - agent_settings: task_create_params.AgentSettings | NotGiven = NOT_GIVEN, - browser_settings: task_create_params.BrowserSettings | NotGiven = NOT_GIVEN, - included_file_names: Optional[List[str]] | NotGiven = NOT_GIVEN, - metadata: Optional[Dict[str, str]] | NotGiven = NOT_GIVEN, - secrets: Optional[Dict[str, str]] | NotGiven = NOT_GIVEN, - structured_output_json: Optional[Union[type[BaseModel], str]] | 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, - ) -> TaskCreateResponse: - """ - Create and start a new Browser Use Agent task. - - This is the main endpoint for running AI agents. You can either: - - 1. Start a new session with a new task. - 2. Add a follow-up task to an existing session. - - When starting a new session: - - - A new browser session is created - - Credits are deducted from your account - - The agent begins executing your task immediately - - When adding to an existing session: - - - The agent continues in the same browser context - - No additional browser start up costs are charged (browser session is already - active) - - The agent can build on previous work - - Key features: - - - Agent profiles: Define agent behavior and capabilities - - Browser profiles: Control browser settings and environment (only used for new - sessions) - - File uploads: Include documents for the agent to work with - - Structured output: Define the format of the task result - - Task metadata: Add custom data for tracking and organization - - Args: - - - request: Complete task configuration including agent settings, browser - settings, and task description - - Returns: - - - The created task ID together with the task's session ID - - Raises: - - - 402: If user has insufficient credits for a new session - - 404: If referenced agent/browser profiles don't exist - - 400: If session is stopped or already has a running task - - Args: - agent_settings: Configuration settings for the agent - - Attributes: llm: The LLM model to use for the agent start_url: Optional URL to - start the agent on (will not be changed as a step) profile_id: Unique identifier - of the agent profile to use for the task - - browser_settings: Configuration settings for the browser session - - Attributes: session_id: Unique identifier of existing session to continue - profile_id: Unique identifier of browser profile to use (use if you want to - start a new session) - - 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 ( - structured_output_json is not None - and not isinstance(structured_output_json, str) - and isinstance(structured_output_json, type) - ): - structured_output_json = json.dumps(structured_output_json.model_json_schema()) - - return await self._post( - "/tasks", - body=await async_maybe_transform( - { - "task": task, - "agent_settings": agent_settings, - "browser_settings": browser_settings, - "included_file_names": included_file_names, - "metadata": metadata, - "secrets": secrets, - "structured_output_json": structured_output_json, - }, - task_create_params.TaskCreateParams, - ), - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=TaskCreateResponse, - ) - - @overload - async def retrieve( - self, - task_id: str, - structured_output_json: type[T], - *, - # 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, - ) -> TaskViewWithOutput[T]: ... - - @overload - async def retrieve( - self, - task_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, - ) -> TaskView: ... - - async def retrieve( - self, - task_id: str, - structured_output_json: Optional[type[BaseModel]] | 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, - ) -> Union[TaskView, TaskViewWithOutput[BaseModel]]: - """ - Get detailed information about a specific AI agent task. - - Retrieves comprehensive information about a task, including its current status, - progress, and detailed execution data. You can choose to get just the status - (for quick polling) or full details including steps and file information. - - Use this endpoint to: - - - Monitor task progress in real-time - - Review completed task results - - Debug failed tasks by examining steps - - Download output files and logs - - Args: - - - task_id: The unique identifier of the agent task - - Returns: - - - Complete task information - - Raises: - - - 404: If the user agent task doesn't exist - - 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 task_id: - raise ValueError(f"Expected a non-empty value for `task_id` but received {task_id!r}") - - if structured_output_json is not None and isinstance(structured_output_json, type): - res = await self._get( - f"/tasks/{task_id}", - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=TaskView, - ) - - if res.done_output is None: - return TaskViewWithOutput[BaseModel]( - **res.model_dump(), - parsed_output=None, - ) - - parsed_output = structured_output_json.model_validate_json(res.done_output) - - return TaskViewWithOutput[BaseModel]( - **res.model_dump(), - parsed_output=parsed_output, - ) - - return await self._get( - f"/tasks/{task_id}", - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=TaskView, - ) - - @overload - def stream( - self, - task_id: str, - structured_output_json: type[T], - *, - # 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, - ) -> AsyncIterator[TaskViewWithOutput[T]]: ... - - @overload - def stream( - self, - task_id: str, - structured_output_json: None | 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, - ) -> AsyncIterator[TaskView]: ... - - async def stream( - self, - task_id: str, - structured_output_json: type[T] | None | 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, - ) -> AsyncIterator[TaskView | TaskViewWithOutput[T]]: - """ - Stream the task view as it is updated until the task is finished. - """ - - async for res in self._watch( - task_id=task_id, - extra_headers=extra_headers, - extra_query=extra_query, - extra_body=extra_body, - timeout=timeout, - ): - if structured_output_json is not None and isinstance(structured_output_json, type): - if res.done_output is None: - yield TaskViewWithOutput[T]( - **res.model_dump(), - parsed_output=None, - ) - else: - schema: type[T] = structured_output_json - # pydantic returns the model instance, but the type checker can’t infer it. - parsed_output: T = schema.model_validate_json(res.done_output) - yield TaskViewWithOutput[T]( - **res.model_dump(), - parsed_output=parsed_output, - ) - else: - yield res - - async def _watch( - self, - task_id: str, - interval: float = 1, - *, - # 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, - ) -> AsyncIterator[TaskView]: - """Converts a polling loop into a generator loop.""" - prev_hash: str | None = None - - while True: - res = await self.retrieve( - task_id=task_id, - extra_headers=extra_headers, - extra_query=extra_query, - extra_body=extra_body, - timeout=timeout, - ) - - res_hash = hash_task_view(res) - if prev_hash is None or res_hash != prev_hash: - prev_hash = res_hash - yield res - - if res.status == "finished": - break - if res.status == "paused": - break - if res.status == "stopped": - break - if res.status == "started": - await asyncio.sleep(interval) - else: - raise ValueError( - f"Expected one of 'finished', 'paused', 'stopped', or 'started' but received {res.status!r}" - ) - - async def update( - self, - task_id: str, - *, - action: Literal["stop", "pause", "resume", "stop_task_and_session"], - # 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, - ) -> TaskView: - """ - Control the execution of an AI agent task. - - Allows you to pause, resume, or stop tasks, and optionally stop the entire - session. This is useful for: - - - Pausing long-running tasks to review progress - - Stopping tasks that are taking too long - - Ending sessions when you're done with all tasks - - Available actions: - - - STOP: Stop the current task - - PAUSE: Pause the task (can be resumed later) - - RESUME: Resume a paused task - - STOP_TASK_AND_SESSION: Stop the task and end the entire session - - Args: - - - task_id: The unique identifier of the agent task to control - - request: The action to perform on the task - - Returns: - - - The updated task information - - Raises: - - - 404: If the user agent task doesn't exist - - Args: - action: Available actions that can be performed on a task - - Attributes: STOP: Stop the current task execution PAUSE: Pause the current task - execution RESUME: Resume a paused task execution STOP_TASK_AND_SESSION: Stop - both the task and its parent session - - 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 task_id: - raise ValueError(f"Expected a non-empty value for `task_id` but received {task_id!r}") - return await self._patch( - f"/tasks/{task_id}", - body=await async_maybe_transform({"action": action}, task_update_params.TaskUpdateParams), - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=TaskView, - ) - - async def list( - self, - *, - after: Union[str, datetime, None] | NotGiven = NOT_GIVEN, - before: Union[str, datetime, None] | NotGiven = NOT_GIVEN, - filter_by: Optional[TaskStatus] | NotGiven = NOT_GIVEN, - page_number: int | NotGiven = NOT_GIVEN, - page_size: int | NotGiven = NOT_GIVEN, - session_id: Optional[str] | 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, - ) -> TaskListResponse: - """ - Get a paginated list of all Browser Use Agent tasks for the authenticated user. - - Browser Use Agent tasks are the individual jobs that your agents perform within - a session. Each task represents a specific instruction or goal that the agent - works on, such as filling out a form, extracting data, or navigating to specific - pages. - - Returns: - - - A paginated list of Browser Use Agent tasks - - Total count of Browser Use Agent tasks - - Page information for navigation - - Args: - filter_by: Enumeration of possible task execution states - - Attributes: STARTED: Task has been started and is currently running. PAUSED: - Task execution has been temporarily paused (can be resumed) FINISHED: Task has - finished and the agent has completed the task. STOPPED: Task execution has been - manually stopped (cannot be resumed). - - 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( - "/tasks", - options=make_request_options( - extra_headers=extra_headers, - extra_query=extra_query, - extra_body=extra_body, - timeout=timeout, - query=await async_maybe_transform( - { - "after": after, - "before": before, - "filter_by": filter_by, - "page_number": page_number, - "page_size": page_size, - "session_id": session_id, - }, - task_list_params.TaskListParams, - ), - ), - cast_to=TaskListResponse, - ) - - async def get_logs( - self, - task_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, - ) -> TaskGetLogsResponse: - """ - Get a download URL for the execution logs of an AI agent task. - - Task logs contain detailed information about how the AI agent executed the task, - including: - - - Step-by-step reasoning and decisions - - Actions taken on web pages - - Error messages and debugging information - - Performance metrics and timing data - - This is useful for: - - - Understanding how the agent solved the task - - Debugging failed or unexpected results - - Optimizing agent behavior and prompts - - Auditing agent actions for compliance - - Args: - - - task_id: The unique identifier of the agent task - - Returns: - - - A presigned download URL for the task log file - - Raises: - - - 404: If the user agent task doesn't exist - - 500: If the download URL cannot be generated (should not happen) - - 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 task_id: - raise ValueError(f"Expected a non-empty value for `task_id` but received {task_id!r}") - return await self._get( - f"/tasks/{task_id}/logs", - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=TaskGetLogsResponse, - ) - - async def get_output_file( - self, - file_id: str, - *, - task_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, - ) -> TaskGetOutputFileResponse: - """ - Get a download URL for a specific output file generated by an AI agent task. - - AI agents can generate various output files during task execution, such as: - - - Screenshots of web pages - - Extracted data in CSV/JSON format - - Generated reports or documents - - Downloaded files from websites - - This endpoint provides a secure, time-limited download URL for accessing these - files. The URL expires after a short time for security. - - Args: - - - task_id: The unique identifier of the agent task - - file_id: The unique identifier of the output file - - Returns: - - - A presigned download URL for the requested file - - Raises: - - - 404: If the user agent task or output file doesn't exist - - 500: If the download URL cannot be generated (should not happen) - - 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 task_id: - raise ValueError(f"Expected a non-empty value for `task_id` but received {task_id!r}") - if not file_id: - raise ValueError(f"Expected a non-empty value for `file_id` but received {file_id!r}") - return await self._get( - f"/tasks/{task_id}/output-files/{file_id}", - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=TaskGetOutputFileResponse, - ) - - async def get_user_uploaded_file( - self, - file_id: str, - *, - task_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, - ) -> TaskGetUserUploadedFileResponse: - """ - Get a download URL for a specific user uploaded file that was used in the task. - - A user can upload files to their account file bucket and reference the name of - the file in a task. These files are then made available for the agent to use - during the agent task run. - - This endpoint provides a secure, time-limited download URL for accessing these - files. The URL expires after a short time for security. - - Args: - - - task_id: The unique identifier of the agent task - - file_id: The unique identifier of the user uploaded file - - Returns: - - - A presigned download URL for the requested file - - Raises: - - - 404: If the user agent task or user uploaded file doesn't exist - - 500: If the download URL cannot be generated (should not happen) - - 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 task_id: - raise ValueError(f"Expected a non-empty value for `task_id` but received {task_id!r}") - if not file_id: - raise ValueError(f"Expected a non-empty value for `file_id` but received {file_id!r}") - return await self._get( - f"/tasks/{task_id}/user-uploaded-files/{file_id}", - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=TaskGetUserUploadedFileResponse, - ) - - -class TasksResourceWithRawResponse: - def __init__(self, tasks: TasksResource) -> None: - self._tasks = tasks - - self.create = to_raw_response_wrapper( - tasks.create, - ) - self.retrieve = to_raw_response_wrapper( - tasks.retrieve, - ) - self.update = to_raw_response_wrapper( - tasks.update, - ) - self.list = to_raw_response_wrapper( - tasks.list, - ) - self.get_logs = to_raw_response_wrapper( - tasks.get_logs, - ) - self.get_output_file = to_raw_response_wrapper( - tasks.get_output_file, - ) - self.get_user_uploaded_file = to_raw_response_wrapper( - tasks.get_user_uploaded_file, - ) - - -class AsyncTasksResourceWithRawResponse: - def __init__(self, tasks: AsyncTasksResource) -> None: - self._tasks = tasks - - self.create = async_to_raw_response_wrapper( - tasks.create, - ) - self.retrieve = async_to_raw_response_wrapper( - tasks.retrieve, - ) - self.update = async_to_raw_response_wrapper( - tasks.update, - ) - self.list = async_to_raw_response_wrapper( - tasks.list, - ) - self.get_logs = async_to_raw_response_wrapper( - tasks.get_logs, - ) - self.get_output_file = async_to_raw_response_wrapper( - tasks.get_output_file, - ) - self.get_user_uploaded_file = async_to_raw_response_wrapper( - tasks.get_user_uploaded_file, - ) - - -class TasksResourceWithStreamingResponse: - def __init__(self, tasks: TasksResource) -> None: - self._tasks = tasks - - self.create = to_streamed_response_wrapper( - tasks.create, - ) - self.retrieve = to_streamed_response_wrapper( - tasks.retrieve, - ) - self.update = to_streamed_response_wrapper( - tasks.update, - ) - self.list = to_streamed_response_wrapper( - tasks.list, - ) - self.get_logs = to_streamed_response_wrapper( - tasks.get_logs, - ) - self.get_output_file = to_streamed_response_wrapper( - tasks.get_output_file, - ) - self.get_user_uploaded_file = to_streamed_response_wrapper( - tasks.get_user_uploaded_file, - ) - - -class AsyncTasksResourceWithStreamingResponse: - def __init__(self, tasks: AsyncTasksResource) -> None: - self._tasks = tasks - - self.create = async_to_streamed_response_wrapper( - tasks.create, - ) - self.retrieve = async_to_streamed_response_wrapper( - tasks.retrieve, - ) - self.update = async_to_streamed_response_wrapper( - tasks.update, - ) - self.list = async_to_streamed_response_wrapper( - tasks.list, - ) - self.get_logs = async_to_streamed_response_wrapper( - tasks.get_logs, - ) - self.get_output_file = async_to_streamed_response_wrapper( - tasks.get_output_file, - ) - self.get_user_uploaded_file = async_to_streamed_response_wrapper( - tasks.get_user_uploaded_file, - ) diff --git a/src/browser_use_sdk/resources/users/__init__.py b/src/browser_use_sdk/resources/users/__init__.py deleted file mode 100644 index 8b1ed20..0000000 --- a/src/browser_use_sdk/resources/users/__init__.py +++ /dev/null @@ -1,33 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from .me import ( - MeResource, - AsyncMeResource, - MeResourceWithRawResponse, - AsyncMeResourceWithRawResponse, - MeResourceWithStreamingResponse, - AsyncMeResourceWithStreamingResponse, -) -from .users import ( - UsersResource, - AsyncUsersResource, - UsersResourceWithRawResponse, - AsyncUsersResourceWithRawResponse, - UsersResourceWithStreamingResponse, - AsyncUsersResourceWithStreamingResponse, -) - -__all__ = [ - "MeResource", - "AsyncMeResource", - "MeResourceWithRawResponse", - "AsyncMeResourceWithRawResponse", - "MeResourceWithStreamingResponse", - "AsyncMeResourceWithStreamingResponse", - "UsersResource", - "AsyncUsersResource", - "UsersResourceWithRawResponse", - "AsyncUsersResourceWithRawResponse", - "UsersResourceWithStreamingResponse", - "AsyncUsersResourceWithStreamingResponse", -] diff --git a/src/browser_use_sdk/resources/users/me/__init__.py b/src/browser_use_sdk/resources/users/me/__init__.py deleted file mode 100644 index 4409f6d..0000000 --- a/src/browser_use_sdk/resources/users/me/__init__.py +++ /dev/null @@ -1,33 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from .me import ( - MeResource, - AsyncMeResource, - MeResourceWithRawResponse, - AsyncMeResourceWithRawResponse, - MeResourceWithStreamingResponse, - AsyncMeResourceWithStreamingResponse, -) -from .files import ( - FilesResource, - AsyncFilesResource, - FilesResourceWithRawResponse, - AsyncFilesResourceWithRawResponse, - FilesResourceWithStreamingResponse, - AsyncFilesResourceWithStreamingResponse, -) - -__all__ = [ - "FilesResource", - "AsyncFilesResource", - "FilesResourceWithRawResponse", - "AsyncFilesResourceWithRawResponse", - "FilesResourceWithStreamingResponse", - "AsyncFilesResourceWithStreamingResponse", - "MeResource", - "AsyncMeResource", - "MeResourceWithRawResponse", - "AsyncMeResourceWithRawResponse", - "MeResourceWithStreamingResponse", - "AsyncMeResourceWithStreamingResponse", -] diff --git a/src/browser_use_sdk/resources/users/me/files.py b/src/browser_use_sdk/resources/users/me/files.py deleted file mode 100644 index 1468254..0000000 --- a/src/browser_use_sdk/resources/users/me/files.py +++ /dev/null @@ -1,269 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -from typing_extensions import Literal - -import httpx - -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.users.me import file_create_presigned_url_params -from ....types.users.me.file_create_presigned_url_response import FileCreatePresignedURLResponse - -__all__ = ["FilesResource", "AsyncFilesResource"] - - -class FilesResource(SyncAPIResource): - @cached_property - def with_raw_response(self) -> FilesResourceWithRawResponse: - """ - 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/browser-use/browser-use-python#accessing-raw-response-data-eg-headers - """ - return FilesResourceWithRawResponse(self) - - @cached_property - def with_streaming_response(self) -> FilesResourceWithStreamingResponse: - """ - An alternative to `.with_raw_response` that doesn't eagerly read the response body. - - For more information, see https://www.github.com/browser-use/browser-use-python#with_streaming_response - """ - return FilesResourceWithStreamingResponse(self) - - def create_presigned_url( - self, - *, - content_type: Literal[ - "image/jpg", - "image/jpeg", - "image/png", - "image/gif", - "image/webp", - "image/svg+xml", - "application/pdf", - "application/msword", - "application/vnd.openxmlformats-officedocument.wordprocessingml.document", - "application/vnd.ms-excel", - "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", - "text/plain", - "text/csv", - "text/markdown", - ], - file_name: str, - size_bytes: int, - # 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, - ) -> FileCreatePresignedURLResponse: - """ - Get a presigned URL for uploading files that AI agents can use during tasks. - - This endpoint generates a secure, time-limited upload URL that allows you to - upload files directly to our storage system. These files can then be referenced - in AI agent tasks for the agent to work with. - - Supported use cases: - - - Uploading documents for data extraction tasks - - Providing reference materials for agents - - Sharing files that agents need to process - - Including images or PDFs for analysis - - The upload URL expires after 2 minutes for security. Files are automatically - organized by user ID and can be referenced in task creation using the returned - file name. - - Args: - - - request: File upload details including name, content type, and size - - Returns: - - - Presigned upload URL and form fields for direct file upload - - Raises: - - - 400: If the content type is unsupported - - 500: If the upload URL generation fails (should not happen) - - 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._post( - "/users/me/files/presigned-url", - body=maybe_transform( - { - "content_type": content_type, - "file_name": file_name, - "size_bytes": size_bytes, - }, - file_create_presigned_url_params.FileCreatePresignedURLParams, - ), - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=FileCreatePresignedURLResponse, - ) - - -class AsyncFilesResource(AsyncAPIResource): - @cached_property - def with_raw_response(self) -> AsyncFilesResourceWithRawResponse: - """ - 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/browser-use/browser-use-python#accessing-raw-response-data-eg-headers - """ - return AsyncFilesResourceWithRawResponse(self) - - @cached_property - def with_streaming_response(self) -> AsyncFilesResourceWithStreamingResponse: - """ - An alternative to `.with_raw_response` that doesn't eagerly read the response body. - - For more information, see https://www.github.com/browser-use/browser-use-python#with_streaming_response - """ - return AsyncFilesResourceWithStreamingResponse(self) - - async def create_presigned_url( - self, - *, - content_type: Literal[ - "image/jpg", - "image/jpeg", - "image/png", - "image/gif", - "image/webp", - "image/svg+xml", - "application/pdf", - "application/msword", - "application/vnd.openxmlformats-officedocument.wordprocessingml.document", - "application/vnd.ms-excel", - "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", - "text/plain", - "text/csv", - "text/markdown", - ], - file_name: str, - size_bytes: int, - # 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, - ) -> FileCreatePresignedURLResponse: - """ - Get a presigned URL for uploading files that AI agents can use during tasks. - - This endpoint generates a secure, time-limited upload URL that allows you to - upload files directly to our storage system. These files can then be referenced - in AI agent tasks for the agent to work with. - - Supported use cases: - - - Uploading documents for data extraction tasks - - Providing reference materials for agents - - Sharing files that agents need to process - - Including images or PDFs for analysis - - The upload URL expires after 2 minutes for security. Files are automatically - organized by user ID and can be referenced in task creation using the returned - file name. - - Args: - - - request: File upload details including name, content type, and size - - Returns: - - - Presigned upload URL and form fields for direct file upload - - Raises: - - - 400: If the content type is unsupported - - 500: If the upload URL generation fails (should not happen) - - 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._post( - "/users/me/files/presigned-url", - body=await async_maybe_transform( - { - "content_type": content_type, - "file_name": file_name, - "size_bytes": size_bytes, - }, - file_create_presigned_url_params.FileCreatePresignedURLParams, - ), - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=FileCreatePresignedURLResponse, - ) - - -class FilesResourceWithRawResponse: - def __init__(self, files: FilesResource) -> None: - self._files = files - - self.create_presigned_url = to_raw_response_wrapper( - files.create_presigned_url, - ) - - -class AsyncFilesResourceWithRawResponse: - def __init__(self, files: AsyncFilesResource) -> None: - self._files = files - - self.create_presigned_url = async_to_raw_response_wrapper( - files.create_presigned_url, - ) - - -class FilesResourceWithStreamingResponse: - def __init__(self, files: FilesResource) -> None: - self._files = files - - self.create_presigned_url = to_streamed_response_wrapper( - files.create_presigned_url, - ) - - -class AsyncFilesResourceWithStreamingResponse: - def __init__(self, files: AsyncFilesResource) -> None: - self._files = files - - self.create_presigned_url = async_to_streamed_response_wrapper( - files.create_presigned_url, - ) diff --git a/src/browser_use_sdk/resources/users/me/me.py b/src/browser_use_sdk/resources/users/me/me.py deleted file mode 100644 index b63585c..0000000 --- a/src/browser_use_sdk/resources/users/me/me.py +++ /dev/null @@ -1,207 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -import httpx - -from .files import ( - FilesResource, - AsyncFilesResource, - FilesResourceWithRawResponse, - AsyncFilesResourceWithRawResponse, - FilesResourceWithStreamingResponse, - AsyncFilesResourceWithStreamingResponse, -) -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.users.me_retrieve_response import MeRetrieveResponse - -__all__ = ["MeResource", "AsyncMeResource"] - - -class MeResource(SyncAPIResource): - @cached_property - def files(self) -> FilesResource: - return FilesResource(self._client) - - @cached_property - def with_raw_response(self) -> MeResourceWithRawResponse: - """ - 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/browser-use/browser-use-python#accessing-raw-response-data-eg-headers - """ - return MeResourceWithRawResponse(self) - - @cached_property - def with_streaming_response(self) -> MeResourceWithStreamingResponse: - """ - An alternative to `.with_raw_response` that doesn't eagerly read the response body. - - For more information, see https://www.github.com/browser-use/browser-use-python#with_streaming_response - """ - return MeResourceWithStreamingResponse(self) - - def retrieve( - 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, - ) -> MeRetrieveResponse: - """ - Get information about the currently authenticated user. - - Retrieves your user profile information including: - - - Credit balances (monthly and additional credits in USD) - - Account details (email, name, signup date) - - This endpoint is useful for: - - - Checking your remaining credits before running tasks - - Displaying user information in your application - - Returns: - - - Complete user profile information including credits and account details - - Raises: - - - 404: If the user profile cannot be found - """ - return self._get( - "/users/me", - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=MeRetrieveResponse, - ) - - -class AsyncMeResource(AsyncAPIResource): - @cached_property - def files(self) -> AsyncFilesResource: - return AsyncFilesResource(self._client) - - @cached_property - def with_raw_response(self) -> AsyncMeResourceWithRawResponse: - """ - 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/browser-use/browser-use-python#accessing-raw-response-data-eg-headers - """ - return AsyncMeResourceWithRawResponse(self) - - @cached_property - def with_streaming_response(self) -> AsyncMeResourceWithStreamingResponse: - """ - An alternative to `.with_raw_response` that doesn't eagerly read the response body. - - For more information, see https://www.github.com/browser-use/browser-use-python#with_streaming_response - """ - return AsyncMeResourceWithStreamingResponse(self) - - async def retrieve( - 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, - ) -> MeRetrieveResponse: - """ - Get information about the currently authenticated user. - - Retrieves your user profile information including: - - - Credit balances (monthly and additional credits in USD) - - Account details (email, name, signup date) - - This endpoint is useful for: - - - Checking your remaining credits before running tasks - - Displaying user information in your application - - Returns: - - - Complete user profile information including credits and account details - - Raises: - - - 404: If the user profile cannot be found - """ - return await self._get( - "/users/me", - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=MeRetrieveResponse, - ) - - -class MeResourceWithRawResponse: - def __init__(self, me: MeResource) -> None: - self._me = me - - self.retrieve = to_raw_response_wrapper( - me.retrieve, - ) - - @cached_property - def files(self) -> FilesResourceWithRawResponse: - return FilesResourceWithRawResponse(self._me.files) - - -class AsyncMeResourceWithRawResponse: - def __init__(self, me: AsyncMeResource) -> None: - self._me = me - - self.retrieve = async_to_raw_response_wrapper( - me.retrieve, - ) - - @cached_property - def files(self) -> AsyncFilesResourceWithRawResponse: - return AsyncFilesResourceWithRawResponse(self._me.files) - - -class MeResourceWithStreamingResponse: - def __init__(self, me: MeResource) -> None: - self._me = me - - self.retrieve = to_streamed_response_wrapper( - me.retrieve, - ) - - @cached_property - def files(self) -> FilesResourceWithStreamingResponse: - return FilesResourceWithStreamingResponse(self._me.files) - - -class AsyncMeResourceWithStreamingResponse: - def __init__(self, me: AsyncMeResource) -> None: - self._me = me - - self.retrieve = async_to_streamed_response_wrapper( - me.retrieve, - ) - - @cached_property - def files(self) -> AsyncFilesResourceWithStreamingResponse: - return AsyncFilesResourceWithStreamingResponse(self._me.files) diff --git a/src/browser_use_sdk/resources/users/users.py b/src/browser_use_sdk/resources/users/users.py deleted file mode 100644 index 95dfc93..0000000 --- a/src/browser_use_sdk/resources/users/users.py +++ /dev/null @@ -1,102 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -from .me.me import ( - MeResource, - AsyncMeResource, - MeResourceWithRawResponse, - AsyncMeResourceWithRawResponse, - MeResourceWithStreamingResponse, - AsyncMeResourceWithStreamingResponse, -) -from ..._compat import cached_property -from ..._resource import SyncAPIResource, AsyncAPIResource - -__all__ = ["UsersResource", "AsyncUsersResource"] - - -class UsersResource(SyncAPIResource): - @cached_property - def me(self) -> MeResource: - return MeResource(self._client) - - @cached_property - def with_raw_response(self) -> UsersResourceWithRawResponse: - """ - 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/browser-use/browser-use-python#accessing-raw-response-data-eg-headers - """ - return UsersResourceWithRawResponse(self) - - @cached_property - def with_streaming_response(self) -> UsersResourceWithStreamingResponse: - """ - An alternative to `.with_raw_response` that doesn't eagerly read the response body. - - For more information, see https://www.github.com/browser-use/browser-use-python#with_streaming_response - """ - return UsersResourceWithStreamingResponse(self) - - -class AsyncUsersResource(AsyncAPIResource): - @cached_property - def me(self) -> AsyncMeResource: - return AsyncMeResource(self._client) - - @cached_property - def with_raw_response(self) -> AsyncUsersResourceWithRawResponse: - """ - 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/browser-use/browser-use-python#accessing-raw-response-data-eg-headers - """ - return AsyncUsersResourceWithRawResponse(self) - - @cached_property - def with_streaming_response(self) -> AsyncUsersResourceWithStreamingResponse: - """ - An alternative to `.with_raw_response` that doesn't eagerly read the response body. - - For more information, see https://www.github.com/browser-use/browser-use-python#with_streaming_response - """ - return AsyncUsersResourceWithStreamingResponse(self) - - -class UsersResourceWithRawResponse: - def __init__(self, users: UsersResource) -> None: - self._users = users - - @cached_property - def me(self) -> MeResourceWithRawResponse: - return MeResourceWithRawResponse(self._users.me) - - -class AsyncUsersResourceWithRawResponse: - def __init__(self, users: AsyncUsersResource) -> None: - self._users = users - - @cached_property - def me(self) -> AsyncMeResourceWithRawResponse: - return AsyncMeResourceWithRawResponse(self._users.me) - - -class UsersResourceWithStreamingResponse: - def __init__(self, users: UsersResource) -> None: - self._users = users - - @cached_property - def me(self) -> MeResourceWithStreamingResponse: - return MeResourceWithStreamingResponse(self._users.me) - - -class AsyncUsersResourceWithStreamingResponse: - def __init__(self, users: AsyncUsersResource) -> None: - self._users = users - - @cached_property - def me(self) -> AsyncMeResourceWithStreamingResponse: - return AsyncMeResourceWithStreamingResponse(self._users.me) diff --git a/src/browser_use_sdk/types/__init__.py b/src/browser_use_sdk/types/__init__.py deleted file mode 100644 index 191c708..0000000 --- a/src/browser_use_sdk/types/__init__.py +++ /dev/null @@ -1,33 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -from .file_view import FileView as FileView -from .task_view import TaskView as TaskView -from .task_status import TaskStatus as TaskStatus -from .session_view import SessionView as SessionView -from .session_status import SessionStatus as SessionStatus -from .task_item_view import TaskItemView as TaskItemView -from .task_step_view import TaskStepView as TaskStepView -from .task_list_params import TaskListParams as TaskListParams -from .agent_profile_view import AgentProfileView as AgentProfileView -from .proxy_country_code import ProxyCountryCode as ProxyCountryCode -from .task_create_params import TaskCreateParams as TaskCreateParams -from .task_list_response import TaskListResponse as TaskListResponse -from .task_update_params import TaskUpdateParams as TaskUpdateParams -from .session_list_params import SessionListParams as SessionListParams -from .browser_profile_view import BrowserProfileView as BrowserProfileView -from .task_create_response import TaskCreateResponse as TaskCreateResponse -from .session_list_response import SessionListResponse as SessionListResponse -from .session_update_params import SessionUpdateParams as SessionUpdateParams -from .task_get_logs_response import TaskGetLogsResponse as TaskGetLogsResponse -from .agent_profile_list_params import AgentProfileListParams as AgentProfileListParams -from .agent_profile_create_params import AgentProfileCreateParams as AgentProfileCreateParams -from .agent_profile_list_response import AgentProfileListResponse as AgentProfileListResponse -from .agent_profile_update_params import AgentProfileUpdateParams as AgentProfileUpdateParams -from .browser_profile_list_params import BrowserProfileListParams as BrowserProfileListParams -from .browser_profile_create_params import BrowserProfileCreateParams as BrowserProfileCreateParams -from .browser_profile_list_response import BrowserProfileListResponse as BrowserProfileListResponse -from .browser_profile_update_params import BrowserProfileUpdateParams as BrowserProfileUpdateParams -from .task_get_output_file_response import TaskGetOutputFileResponse as TaskGetOutputFileResponse -from .task_get_user_uploaded_file_response import TaskGetUserUploadedFileResponse as TaskGetUserUploadedFileResponse diff --git a/src/browser_use_sdk/types/agent_profile_create_params.py b/src/browser_use_sdk/types/agent_profile_create_params.py deleted file mode 100644 index ef3080b..0000000 --- a/src/browser_use_sdk/types/agent_profile_create_params.py +++ /dev/null @@ -1,30 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -from typing import List -from typing_extensions import Required, Annotated, TypedDict - -from .._utils import PropertyInfo - -__all__ = ["AgentProfileCreateParams"] - - -class AgentProfileCreateParams(TypedDict, total=False): - name: Required[str] - - allowed_domains: Annotated[List[str], PropertyInfo(alias="allowedDomains")] - - custom_system_prompt_extension: Annotated[str, PropertyInfo(alias="customSystemPromptExtension")] - - description: str - - flash_mode: Annotated[bool, PropertyInfo(alias="flashMode")] - - highlight_elements: Annotated[bool, PropertyInfo(alias="highlightElements")] - - max_agent_steps: Annotated[int, PropertyInfo(alias="maxAgentSteps")] - - thinking: bool - - vision: bool diff --git a/src/browser_use_sdk/types/agent_profile_list_params.py b/src/browser_use_sdk/types/agent_profile_list_params.py deleted file mode 100644 index 072292c..0000000 --- a/src/browser_use_sdk/types/agent_profile_list_params.py +++ /dev/null @@ -1,15 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -from typing_extensions import Annotated, TypedDict - -from .._utils import PropertyInfo - -__all__ = ["AgentProfileListParams"] - - -class AgentProfileListParams(TypedDict, total=False): - page_number: Annotated[int, PropertyInfo(alias="pageNumber")] - - page_size: Annotated[int, PropertyInfo(alias="pageSize")] diff --git a/src/browser_use_sdk/types/agent_profile_list_response.py b/src/browser_use_sdk/types/agent_profile_list_response.py deleted file mode 100644 index 3f77c1a..0000000 --- a/src/browser_use_sdk/types/agent_profile_list_response.py +++ /dev/null @@ -1,20 +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 -from .agent_profile_view import AgentProfileView - -__all__ = ["AgentProfileListResponse"] - - -class AgentProfileListResponse(BaseModel): - items: List[AgentProfileView] - - page_number: int = FieldInfo(alias="pageNumber") - - page_size: int = FieldInfo(alias="pageSize") - - total_items: int = FieldInfo(alias="totalItems") diff --git a/src/browser_use_sdk/types/agent_profile_update_params.py b/src/browser_use_sdk/types/agent_profile_update_params.py deleted file mode 100644 index 8595815..0000000 --- a/src/browser_use_sdk/types/agent_profile_update_params.py +++ /dev/null @@ -1,30 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -from typing import List, Optional -from typing_extensions import Annotated, TypedDict - -from .._utils import PropertyInfo - -__all__ = ["AgentProfileUpdateParams"] - - -class AgentProfileUpdateParams(TypedDict, total=False): - allowed_domains: Annotated[Optional[List[str]], PropertyInfo(alias="allowedDomains")] - - custom_system_prompt_extension: Annotated[Optional[str], PropertyInfo(alias="customSystemPromptExtension")] - - description: Optional[str] - - flash_mode: Annotated[Optional[bool], PropertyInfo(alias="flashMode")] - - highlight_elements: Annotated[Optional[bool], PropertyInfo(alias="highlightElements")] - - max_agent_steps: Annotated[Optional[int], PropertyInfo(alias="maxAgentSteps")] - - name: Optional[str] - - thinking: Optional[bool] - - vision: Optional[bool] diff --git a/src/browser_use_sdk/types/agent_profile_view.py b/src/browser_use_sdk/types/agent_profile_view.py deleted file mode 100644 index 1c10f12..0000000 --- a/src/browser_use_sdk/types/agent_profile_view.py +++ /dev/null @@ -1,36 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from typing import List -from datetime import datetime - -from pydantic import Field as FieldInfo - -from .._models import BaseModel - -__all__ = ["AgentProfileView"] - - -class AgentProfileView(BaseModel): - id: str - - allowed_domains: List[str] = FieldInfo(alias="allowedDomains") - - created_at: datetime = FieldInfo(alias="createdAt") - - custom_system_prompt_extension: str = FieldInfo(alias="customSystemPromptExtension") - - description: str - - flash_mode: bool = FieldInfo(alias="flashMode") - - highlight_elements: bool = FieldInfo(alias="highlightElements") - - max_agent_steps: int = FieldInfo(alias="maxAgentSteps") - - name: str - - thinking: bool - - updated_at: datetime = FieldInfo(alias="updatedAt") - - vision: bool diff --git a/src/browser_use_sdk/types/browser_profile_create_params.py b/src/browser_use_sdk/types/browser_profile_create_params.py deleted file mode 100644 index 57a2e97..0000000 --- a/src/browser_use_sdk/types/browser_profile_create_params.py +++ /dev/null @@ -1,32 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -from typing_extensions import Required, Annotated, TypedDict - -from .._utils import PropertyInfo -from .proxy_country_code import ProxyCountryCode - -__all__ = ["BrowserProfileCreateParams"] - - -class BrowserProfileCreateParams(TypedDict, total=False): - name: Required[str] - - ad_blocker: Annotated[bool, PropertyInfo(alias="adBlocker")] - - browser_viewport_height: Annotated[int, PropertyInfo(alias="browserViewportHeight")] - - browser_viewport_width: Annotated[int, PropertyInfo(alias="browserViewportWidth")] - - description: str - - is_mobile: Annotated[bool, PropertyInfo(alias="isMobile")] - - persist: bool - - proxy: bool - - proxy_country_code: Annotated[ProxyCountryCode, PropertyInfo(alias="proxyCountryCode")] - - store_cache: Annotated[bool, PropertyInfo(alias="storeCache")] diff --git a/src/browser_use_sdk/types/browser_profile_list_params.py b/src/browser_use_sdk/types/browser_profile_list_params.py deleted file mode 100644 index bb03c96..0000000 --- a/src/browser_use_sdk/types/browser_profile_list_params.py +++ /dev/null @@ -1,15 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -from typing_extensions import Annotated, TypedDict - -from .._utils import PropertyInfo - -__all__ = ["BrowserProfileListParams"] - - -class BrowserProfileListParams(TypedDict, total=False): - page_number: Annotated[int, PropertyInfo(alias="pageNumber")] - - page_size: Annotated[int, PropertyInfo(alias="pageSize")] diff --git a/src/browser_use_sdk/types/browser_profile_list_response.py b/src/browser_use_sdk/types/browser_profile_list_response.py deleted file mode 100644 index 02d22ae..0000000 --- a/src/browser_use_sdk/types/browser_profile_list_response.py +++ /dev/null @@ -1,20 +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 -from .browser_profile_view import BrowserProfileView - -__all__ = ["BrowserProfileListResponse"] - - -class BrowserProfileListResponse(BaseModel): - items: List[BrowserProfileView] - - page_number: int = FieldInfo(alias="pageNumber") - - page_size: int = FieldInfo(alias="pageSize") - - total_items: int = FieldInfo(alias="totalItems") diff --git a/src/browser_use_sdk/types/browser_profile_update_params.py b/src/browser_use_sdk/types/browser_profile_update_params.py deleted file mode 100644 index 5305fa1..0000000 --- a/src/browser_use_sdk/types/browser_profile_update_params.py +++ /dev/null @@ -1,33 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -from typing import Optional -from typing_extensions import Annotated, TypedDict - -from .._utils import PropertyInfo -from .proxy_country_code import ProxyCountryCode - -__all__ = ["BrowserProfileUpdateParams"] - - -class BrowserProfileUpdateParams(TypedDict, total=False): - ad_blocker: Annotated[Optional[bool], PropertyInfo(alias="adBlocker")] - - browser_viewport_height: Annotated[Optional[int], PropertyInfo(alias="browserViewportHeight")] - - browser_viewport_width: Annotated[Optional[int], PropertyInfo(alias="browserViewportWidth")] - - description: Optional[str] - - is_mobile: Annotated[Optional[bool], PropertyInfo(alias="isMobile")] - - name: Optional[str] - - persist: Optional[bool] - - proxy: Optional[bool] - - proxy_country_code: Annotated[Optional[ProxyCountryCode], PropertyInfo(alias="proxyCountryCode")] - - store_cache: Annotated[Optional[bool], PropertyInfo(alias="storeCache")] diff --git a/src/browser_use_sdk/types/browser_profile_view.py b/src/browser_use_sdk/types/browser_profile_view.py deleted file mode 100644 index b9384ed..0000000 --- a/src/browser_use_sdk/types/browser_profile_view.py +++ /dev/null @@ -1,38 +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 -from .proxy_country_code import ProxyCountryCode - -__all__ = ["BrowserProfileView"] - - -class BrowserProfileView(BaseModel): - id: str - - ad_blocker: bool = FieldInfo(alias="adBlocker") - - browser_viewport_height: int = FieldInfo(alias="browserViewportHeight") - - browser_viewport_width: int = FieldInfo(alias="browserViewportWidth") - - created_at: datetime = FieldInfo(alias="createdAt") - - description: str - - is_mobile: bool = FieldInfo(alias="isMobile") - - name: str - - persist: bool - - proxy: bool - - proxy_country_code: ProxyCountryCode = FieldInfo(alias="proxyCountryCode") - - store_cache: bool = FieldInfo(alias="storeCache") - - updated_at: datetime = FieldInfo(alias="updatedAt") diff --git a/src/browser_use_sdk/types/file_view.py b/src/browser_use_sdk/types/file_view.py deleted file mode 100644 index 620c57c..0000000 --- a/src/browser_use_sdk/types/file_view.py +++ /dev/null @@ -1,13 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from pydantic import Field as FieldInfo - -from .._models import BaseModel - -__all__ = ["FileView"] - - -class FileView(BaseModel): - id: str - - file_name: str = FieldInfo(alias="fileName") diff --git a/src/browser_use_sdk/types/proxy_country_code.py b/src/browser_use_sdk/types/proxy_country_code.py deleted file mode 100644 index b60c0fa..0000000 --- a/src/browser_use_sdk/types/proxy_country_code.py +++ /dev/null @@ -1,7 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from typing_extensions import Literal, TypeAlias - -__all__ = ["ProxyCountryCode"] - -ProxyCountryCode: TypeAlias = Literal["us", "uk", "fr", "it", "jp", "au", "de", "fi", "ca", "in"] diff --git a/src/browser_use_sdk/types/session_list_params.py b/src/browser_use_sdk/types/session_list_params.py deleted file mode 100644 index 0bd7b72..0000000 --- a/src/browser_use_sdk/types/session_list_params.py +++ /dev/null @@ -1,24 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -from typing import Optional -from typing_extensions import Annotated, TypedDict - -from .._utils import PropertyInfo -from .session_status import SessionStatus - -__all__ = ["SessionListParams"] - - -class SessionListParams(TypedDict, total=False): - filter_by: Annotated[Optional[SessionStatus], PropertyInfo(alias="filterBy")] - """Enumeration of possible (browser) session states - - Attributes: ACTIVE: Session is currently active and running (browser is running) - STOPPED: Session has been stopped and is no longer active (browser is stopped) - """ - - page_number: Annotated[int, PropertyInfo(alias="pageNumber")] - - page_size: Annotated[int, PropertyInfo(alias="pageSize")] diff --git a/src/browser_use_sdk/types/session_list_response.py b/src/browser_use_sdk/types/session_list_response.py deleted file mode 100644 index 0958cfa..0000000 --- a/src/browser_use_sdk/types/session_list_response.py +++ /dev/null @@ -1,38 +0,0 @@ -# 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 -from .session_status import SessionStatus - -__all__ = ["SessionListResponse", "Item"] - - -class Item(BaseModel): - id: str - - started_at: datetime = FieldInfo(alias="startedAt") - - status: SessionStatus - """Enumeration of possible (browser) session states - - Attributes: ACTIVE: Session is currently active and running (browser is running) - STOPPED: Session has been stopped and is no longer active (browser is stopped) - """ - - finished_at: Optional[datetime] = FieldInfo(alias="finishedAt", default=None) - - live_url: Optional[str] = FieldInfo(alias="liveUrl", default=None) - - -class SessionListResponse(BaseModel): - items: List[Item] - - page_number: int = FieldInfo(alias="pageNumber") - - page_size: int = FieldInfo(alias="pageSize") - - total_items: int = FieldInfo(alias="totalItems") diff --git a/src/browser_use_sdk/types/session_status.py b/src/browser_use_sdk/types/session_status.py deleted file mode 100644 index dc8ad95..0000000 --- a/src/browser_use_sdk/types/session_status.py +++ /dev/null @@ -1,7 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from typing_extensions import Literal, TypeAlias - -__all__ = ["SessionStatus"] - -SessionStatus: TypeAlias = Literal["active", "stopped"] diff --git a/src/browser_use_sdk/types/session_update_params.py b/src/browser_use_sdk/types/session_update_params.py deleted file mode 100644 index 92734a7..0000000 --- a/src/browser_use_sdk/types/session_update_params.py +++ /dev/null @@ -1,16 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -from typing_extensions import Literal, Required, TypedDict - -__all__ = ["SessionUpdateParams"] - - -class SessionUpdateParams(TypedDict, total=False): - action: Required[Literal["stop"]] - """Available actions that can be performed on a session - - Attributes: STOP: Stop the session and all its associated tasks (cannot be - undone) - """ diff --git a/src/browser_use_sdk/types/session_view.py b/src/browser_use_sdk/types/session_view.py deleted file mode 100644 index 492e4aa..0000000 --- a/src/browser_use_sdk/types/session_view.py +++ /dev/null @@ -1,35 +0,0 @@ -# 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 -from .session_status import SessionStatus -from .task_item_view import TaskItemView - -__all__ = ["SessionView"] - - -class SessionView(BaseModel): - id: str - - started_at: datetime = FieldInfo(alias="startedAt") - - status: SessionStatus - """Enumeration of possible (browser) session states - - Attributes: ACTIVE: Session is currently active and running (browser is running) - STOPPED: Session has been stopped and is no longer active (browser is stopped) - """ - - finished_at: Optional[datetime] = FieldInfo(alias="finishedAt", default=None) - - live_url: Optional[str] = FieldInfo(alias="liveUrl", default=None) - - public_share_url: Optional[str] = FieldInfo(alias="publicShareUrl", default=None) - - record_url: Optional[str] = FieldInfo(alias="recordUrl", default=None) - - tasks: Optional[List[TaskItemView]] = None diff --git a/src/browser_use_sdk/types/sessions/__init__.py b/src/browser_use_sdk/types/sessions/__init__.py deleted file mode 100644 index ecf1857..0000000 --- a/src/browser_use_sdk/types/sessions/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -from .share_view import ShareView as ShareView diff --git a/src/browser_use_sdk/types/sessions/share_view.py b/src/browser_use_sdk/types/sessions/share_view.py deleted file mode 100644 index 2360441..0000000 --- a/src/browser_use_sdk/types/sessions/share_view.py +++ /dev/null @@ -1,20 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from typing import Optional -from datetime import datetime - -from pydantic import Field as FieldInfo - -from ..._models import BaseModel - -__all__ = ["ShareView"] - - -class ShareView(BaseModel): - share_token: str = FieldInfo(alias="shareToken") - - share_url: str = FieldInfo(alias="shareUrl") - - view_count: int = FieldInfo(alias="viewCount") - - last_viewed_at: Optional[datetime] = FieldInfo(alias="lastViewedAt", default=None) diff --git a/src/browser_use_sdk/types/task_create_params.py b/src/browser_use_sdk/types/task_create_params.py deleted file mode 100644 index 0585011..0000000 --- a/src/browser_use_sdk/types/task_create_params.py +++ /dev/null @@ -1,64 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -from typing import Dict, List, Optional -from typing_extensions import Literal, Required, Annotated, TypedDict - -from .._utils import PropertyInfo - -__all__ = ["TaskCreateParams", "AgentSettings", "BrowserSettings"] - - -class TaskCreateParams(TypedDict, total=False): - task: Required[str] - - agent_settings: Annotated[AgentSettings, PropertyInfo(alias="agentSettings")] - """Configuration settings for the agent - - Attributes: llm: The LLM model to use for the agent start_url: Optional URL to - start the agent on (will not be changed as a step) profile_id: Unique identifier - of the agent profile to use for the task - """ - - browser_settings: Annotated[BrowserSettings, PropertyInfo(alias="browserSettings")] - """Configuration settings for the browser session - - Attributes: session_id: Unique identifier of existing session to continue - profile_id: Unique identifier of browser profile to use (use if you want to - start a new session) - """ - - included_file_names: Annotated[Optional[List[str]], PropertyInfo(alias="includedFileNames")] - - metadata: Optional[Dict[str, str]] - - secrets: Optional[Dict[str, str]] - - structured_output_json: Annotated[Optional[str], PropertyInfo(alias="structuredOutputJson")] - - -class AgentSettings(TypedDict, total=False): - llm: Literal[ - "gpt-4.1", - "gpt-4.1-mini", - "o4-mini", - "o3", - "gemini-2.5-flash", - "gemini-2.5-pro", - "claude-sonnet-4-20250514", - "gpt-4o", - "gpt-4o-mini", - "llama-4-maverick-17b-128e-instruct", - "claude-3-7-sonnet-20250219", - ] - - profile_id: Annotated[Optional[str], PropertyInfo(alias="profileId")] - - start_url: Annotated[Optional[str], PropertyInfo(alias="startUrl")] - - -class BrowserSettings(TypedDict, total=False): - profile_id: Annotated[Optional[str], PropertyInfo(alias="profileId")] - - session_id: Annotated[Optional[str], PropertyInfo(alias="sessionId")] diff --git a/src/browser_use_sdk/types/task_create_response.py b/src/browser_use_sdk/types/task_create_response.py deleted file mode 100644 index b17ab00..0000000 --- a/src/browser_use_sdk/types/task_create_response.py +++ /dev/null @@ -1,13 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from pydantic import Field as FieldInfo - -from .._models import BaseModel - -__all__ = ["TaskCreateResponse"] - - -class TaskCreateResponse(BaseModel): - id: str - - session_id: str = FieldInfo(alias="sessionId") diff --git a/src/browser_use_sdk/types/task_get_logs_response.py b/src/browser_use_sdk/types/task_get_logs_response.py deleted file mode 100644 index 5bc035c..0000000 --- a/src/browser_use_sdk/types/task_get_logs_response.py +++ /dev/null @@ -1,11 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from pydantic import Field as FieldInfo - -from .._models import BaseModel - -__all__ = ["TaskGetLogsResponse"] - - -class TaskGetLogsResponse(BaseModel): - download_url: str = FieldInfo(alias="downloadUrl") diff --git a/src/browser_use_sdk/types/task_get_output_file_response.py b/src/browser_use_sdk/types/task_get_output_file_response.py deleted file mode 100644 index 812ac90..0000000 --- a/src/browser_use_sdk/types/task_get_output_file_response.py +++ /dev/null @@ -1,15 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from pydantic import Field as FieldInfo - -from .._models import BaseModel - -__all__ = ["TaskGetOutputFileResponse"] - - -class TaskGetOutputFileResponse(BaseModel): - id: str - - download_url: str = FieldInfo(alias="downloadUrl") - - file_name: str = FieldInfo(alias="fileName") diff --git a/src/browser_use_sdk/types/task_get_user_uploaded_file_response.py b/src/browser_use_sdk/types/task_get_user_uploaded_file_response.py deleted file mode 100644 index f802207..0000000 --- a/src/browser_use_sdk/types/task_get_user_uploaded_file_response.py +++ /dev/null @@ -1,15 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from pydantic import Field as FieldInfo - -from .._models import BaseModel - -__all__ = ["TaskGetUserUploadedFileResponse"] - - -class TaskGetUserUploadedFileResponse(BaseModel): - id: str - - download_url: str = FieldInfo(alias="downloadUrl") - - file_name: str = FieldInfo(alias="fileName") diff --git a/src/browser_use_sdk/types/task_item_view.py b/src/browser_use_sdk/types/task_item_view.py deleted file mode 100644 index 695e846..0000000 --- a/src/browser_use_sdk/types/task_item_view.py +++ /dev/null @@ -1,44 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from typing import Dict, Optional -from datetime import datetime - -from pydantic import Field as FieldInfo - -from .._models import BaseModel -from .task_status import TaskStatus - -__all__ = ["TaskItemView"] - - -class TaskItemView(BaseModel): - id: str - - is_scheduled: bool = FieldInfo(alias="isScheduled") - - llm: str - - session_id: str = FieldInfo(alias="sessionId") - - started_at: datetime = FieldInfo(alias="startedAt") - - status: TaskStatus - """Enumeration of possible task execution states - - Attributes: STARTED: Task has been started and is currently running. PAUSED: - Task execution has been temporarily paused (can be resumed) FINISHED: Task has - finished and the agent has completed the task. STOPPED: Task execution has been - manually stopped (cannot be resumed). - """ - - task: str - - browser_use_version: Optional[str] = FieldInfo(alias="browserUseVersion", default=None) - - done_output: Optional[str] = FieldInfo(alias="doneOutput", default=None) - - finished_at: Optional[datetime] = FieldInfo(alias="finishedAt", default=None) - - is_success: Optional[bool] = FieldInfo(alias="isSuccess", default=None) - - metadata: Optional[Dict[str, object]] = None diff --git a/src/browser_use_sdk/types/task_list_params.py b/src/browser_use_sdk/types/task_list_params.py deleted file mode 100644 index 0d83aaa..0000000 --- a/src/browser_use_sdk/types/task_list_params.py +++ /dev/null @@ -1,33 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -from typing import Union, Optional -from datetime import datetime -from typing_extensions import Annotated, TypedDict - -from .._utils import PropertyInfo -from .task_status import TaskStatus - -__all__ = ["TaskListParams"] - - -class TaskListParams(TypedDict, total=False): - after: Annotated[Union[str, datetime, None], PropertyInfo(format="iso8601")] - - before: Annotated[Union[str, datetime, None], PropertyInfo(format="iso8601")] - - filter_by: Annotated[Optional[TaskStatus], PropertyInfo(alias="filterBy")] - """Enumeration of possible task execution states - - Attributes: STARTED: Task has been started and is currently running. PAUSED: - Task execution has been temporarily paused (can be resumed) FINISHED: Task has - finished and the agent has completed the task. STOPPED: Task execution has been - manually stopped (cannot be resumed). - """ - - page_number: Annotated[int, PropertyInfo(alias="pageNumber")] - - page_size: Annotated[int, PropertyInfo(alias="pageSize")] - - session_id: Annotated[Optional[str], PropertyInfo(alias="sessionId")] diff --git a/src/browser_use_sdk/types/task_list_response.py b/src/browser_use_sdk/types/task_list_response.py deleted file mode 100644 index de14f6e..0000000 --- a/src/browser_use_sdk/types/task_list_response.py +++ /dev/null @@ -1,20 +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 -from .task_item_view import TaskItemView - -__all__ = ["TaskListResponse"] - - -class TaskListResponse(BaseModel): - items: List[TaskItemView] - - page_number: int = FieldInfo(alias="pageNumber") - - page_size: int = FieldInfo(alias="pageSize") - - total_items: int = FieldInfo(alias="totalItems") diff --git a/src/browser_use_sdk/types/task_status.py b/src/browser_use_sdk/types/task_status.py deleted file mode 100644 index 0eabe70..0000000 --- a/src/browser_use_sdk/types/task_status.py +++ /dev/null @@ -1,7 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from typing_extensions import Literal, TypeAlias - -__all__ = ["TaskStatus"] - -TaskStatus: TypeAlias = Literal["started", "paused", "finished", "stopped"] diff --git a/src/browser_use_sdk/types/task_step_view.py b/src/browser_use_sdk/types/task_step_view.py deleted file mode 100644 index b32e08c..0000000 --- a/src/browser_use_sdk/types/task_step_view.py +++ /dev/null @@ -1,25 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from typing import List, Optional - -from pydantic import Field as FieldInfo - -from .._models import BaseModel - -__all__ = ["TaskStepView"] - - -class TaskStepView(BaseModel): - actions: List[str] - - evaluation_previous_goal: str = FieldInfo(alias="evaluationPreviousGoal") - - memory: str - - next_goal: str = FieldInfo(alias="nextGoal") - - number: int - - url: str - - screenshot_url: Optional[str] = FieldInfo(alias="screenshotUrl", default=None) diff --git a/src/browser_use_sdk/types/task_update_params.py b/src/browser_use_sdk/types/task_update_params.py deleted file mode 100644 index 3ab9614..0000000 --- a/src/browser_use_sdk/types/task_update_params.py +++ /dev/null @@ -1,17 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -from typing_extensions import Literal, Required, TypedDict - -__all__ = ["TaskUpdateParams"] - - -class TaskUpdateParams(TypedDict, total=False): - action: Required[Literal["stop", "pause", "resume", "stop_task_and_session"]] - """Available actions that can be performed on a task - - Attributes: STOP: Stop the current task execution PAUSE: Pause the current task - execution RESUME: Resume a paused task execution STOP_TASK_AND_SESSION: Stop - both the task and its parent session - """ diff --git a/src/browser_use_sdk/types/task_view.py b/src/browser_use_sdk/types/task_view.py deleted file mode 100644 index 6eef227..0000000 --- a/src/browser_use_sdk/types/task_view.py +++ /dev/null @@ -1,79 +0,0 @@ -# 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 - -from pydantic import Field as FieldInfo - -from .._models import BaseModel -from .file_view import FileView -from .task_status import TaskStatus -from .task_step_view import TaskStepView - -__all__ = ["TaskView", "Session"] - - -class Session(BaseModel): - id: str - - started_at: datetime = FieldInfo(alias="startedAt") - - status: Literal["active", "stopped"] - """Enumeration of possible (browser) session states - - Attributes: ACTIVE: Session is currently active and running (browser is running) - STOPPED: Session has been stopped and is no longer active (browser is stopped) - """ - - finished_at: Optional[datetime] = FieldInfo(alias="finishedAt", default=None) - - live_url: Optional[str] = FieldInfo(alias="liveUrl", default=None) - - -class TaskView(BaseModel): - id: str - - is_scheduled: bool = FieldInfo(alias="isScheduled") - - llm: str - - output_files: List[FileView] = FieldInfo(alias="outputFiles") - - session: Session - """View model for representing a session that a task belongs to - - Attributes: id: Unique identifier for the session status: Current status of the - session (active/stopped) live_url: URL where the browser can be viewed live in - real-time. started_at: Timestamp when the session was created and started. - finished_at: Timestamp when the session was stopped (None if still active). - """ - - session_id: str = FieldInfo(alias="sessionId") - - started_at: datetime = FieldInfo(alias="startedAt") - - status: TaskStatus - """Enumeration of possible task execution states - - Attributes: STARTED: Task has been started and is currently running. PAUSED: - Task execution has been temporarily paused (can be resumed) FINISHED: Task has - finished and the agent has completed the task. STOPPED: Task execution has been - manually stopped (cannot be resumed). - """ - - steps: List[TaskStepView] - - task: str - - user_uploaded_files: List[FileView] = FieldInfo(alias="userUploadedFiles") - - browser_use_version: Optional[str] = FieldInfo(alias="browserUseVersion", default=None) - - done_output: Optional[str] = FieldInfo(alias="doneOutput", default=None) - - finished_at: Optional[datetime] = FieldInfo(alias="finishedAt", default=None) - - is_success: Optional[bool] = FieldInfo(alias="isSuccess", default=None) - - metadata: Optional[Dict[str, object]] = None diff --git a/src/browser_use_sdk/types/users/__init__.py b/src/browser_use_sdk/types/users/__init__.py deleted file mode 100644 index de5007b..0000000 --- a/src/browser_use_sdk/types/users/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -from .me_retrieve_response import MeRetrieveResponse as MeRetrieveResponse diff --git a/src/browser_use_sdk/types/users/me/__init__.py b/src/browser_use_sdk/types/users/me/__init__.py deleted file mode 100644 index 27f2334..0000000 --- a/src/browser_use_sdk/types/users/me/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -from .file_create_presigned_url_params import FileCreatePresignedURLParams as FileCreatePresignedURLParams -from .file_create_presigned_url_response import FileCreatePresignedURLResponse as FileCreatePresignedURLResponse diff --git a/src/browser_use_sdk/types/users/me/file_create_presigned_url_params.py b/src/browser_use_sdk/types/users/me/file_create_presigned_url_params.py deleted file mode 100644 index 368b0c9..0000000 --- a/src/browser_use_sdk/types/users/me/file_create_presigned_url_params.py +++ /dev/null @@ -1,37 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -from typing_extensions import Literal, Required, Annotated, TypedDict - -from ...._utils import PropertyInfo - -__all__ = ["FileCreatePresignedURLParams"] - - -class FileCreatePresignedURLParams(TypedDict, total=False): - content_type: Required[ - Annotated[ - Literal[ - "image/jpg", - "image/jpeg", - "image/png", - "image/gif", - "image/webp", - "image/svg+xml", - "application/pdf", - "application/msword", - "application/vnd.openxmlformats-officedocument.wordprocessingml.document", - "application/vnd.ms-excel", - "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", - "text/plain", - "text/csv", - "text/markdown", - ], - PropertyInfo(alias="contentType"), - ] - ] - - file_name: Required[Annotated[str, PropertyInfo(alias="fileName")]] - - size_bytes: Required[Annotated[int, PropertyInfo(alias="sizeBytes")]] diff --git a/src/browser_use_sdk/types/users/me/file_create_presigned_url_response.py b/src/browser_use_sdk/types/users/me/file_create_presigned_url_response.py deleted file mode 100644 index f3dc32f..0000000 --- a/src/browser_use_sdk/types/users/me/file_create_presigned_url_response.py +++ /dev/null @@ -1,22 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from typing import Dict -from typing_extensions import Literal - -from pydantic import Field as FieldInfo - -from ...._models import BaseModel - -__all__ = ["FileCreatePresignedURLResponse"] - - -class FileCreatePresignedURLResponse(BaseModel): - expires_in: int = FieldInfo(alias="expiresIn") - - fields: Dict[str, str] - - file_name: str = FieldInfo(alias="fileName") - - method: Literal["POST"] - - url: str diff --git a/src/browser_use_sdk/types/users/me_retrieve_response.py b/src/browser_use_sdk/types/users/me_retrieve_response.py deleted file mode 100644 index 75d83b0..0000000 --- a/src/browser_use_sdk/types/users/me_retrieve_response.py +++ /dev/null @@ -1,22 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from typing import Optional -from datetime import datetime - -from pydantic import Field as FieldInfo - -from ..._models import BaseModel - -__all__ = ["MeRetrieveResponse"] - - -class MeRetrieveResponse(BaseModel): - additional_credits_balance_usd: float = FieldInfo(alias="additionalCreditsBalanceUsd") - - monthly_credits_balance_usd: float = FieldInfo(alias="monthlyCreditsBalanceUsd") - - signed_up_at: datetime = FieldInfo(alias="signedUpAt") - - email: Optional[str] = None - - name: Optional[str] = None diff --git a/tests/__init__.py b/tests/__init__.py deleted file mode 100644 index fd8019a..0000000 --- a/tests/__init__.py +++ /dev/null @@ -1 +0,0 @@ -# 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 deleted file mode 100644 index fd8019a..0000000 --- a/tests/api_resources/__init__.py +++ /dev/null @@ -1 +0,0 @@ -# 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 deleted file mode 100644 index fd8019a..0000000 --- a/tests/api_resources/sessions/__init__.py +++ /dev/null @@ -1 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. diff --git a/tests/api_resources/sessions/test_public_share.py b/tests/api_resources/sessions/test_public_share.py deleted file mode 100644 index 1b877cb..0000000 --- a/tests/api_resources/sessions/test_public_share.py +++ /dev/null @@ -1,276 +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 pytest - -from tests.utils import assert_matches_type -from browser_use_sdk import BrowserUse, AsyncBrowserUse -from browser_use_sdk.types.sessions import ShareView - -base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") - - -class TestPublicShare: - parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_method_create(self, client: BrowserUse) -> None: - public_share = client.sessions.public_share.create( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) - assert_matches_type(ShareView, public_share, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_raw_response_create(self, client: BrowserUse) -> None: - response = client.sessions.public_share.with_raw_response.create( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - public_share = response.parse() - assert_matches_type(ShareView, public_share, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_streaming_response_create(self, client: BrowserUse) -> None: - with client.sessions.public_share.with_streaming_response.create( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - public_share = response.parse() - assert_matches_type(ShareView, public_share, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_path_params_create(self, client: BrowserUse) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `session_id` but received ''"): - client.sessions.public_share.with_raw_response.create( - "", - ) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_method_retrieve(self, client: BrowserUse) -> None: - public_share = client.sessions.public_share.retrieve( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) - assert_matches_type(ShareView, public_share, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_raw_response_retrieve(self, client: BrowserUse) -> None: - response = client.sessions.public_share.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" - public_share = response.parse() - assert_matches_type(ShareView, public_share, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_streaming_response_retrieve(self, client: BrowserUse) -> None: - with client.sessions.public_share.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" - - public_share = response.parse() - assert_matches_type(ShareView, public_share, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_path_params_retrieve(self, client: BrowserUse) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `session_id` but received ''"): - client.sessions.public_share.with_raw_response.retrieve( - "", - ) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_method_delete(self, client: BrowserUse) -> None: - public_share = client.sessions.public_share.delete( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) - assert public_share is None - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_raw_response_delete(self, client: BrowserUse) -> None: - response = client.sessions.public_share.with_raw_response.delete( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - public_share = response.parse() - assert public_share is None - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_streaming_response_delete(self, client: BrowserUse) -> None: - with client.sessions.public_share.with_streaming_response.delete( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - public_share = response.parse() - assert public_share is None - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_path_params_delete(self, client: BrowserUse) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `session_id` but received ''"): - client.sessions.public_share.with_raw_response.delete( - "", - ) - - -class TestAsyncPublicShare: - parametrize = pytest.mark.parametrize( - "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] - ) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_method_create(self, async_client: AsyncBrowserUse) -> None: - public_share = await async_client.sessions.public_share.create( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) - assert_matches_type(ShareView, public_share, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_raw_response_create(self, async_client: AsyncBrowserUse) -> None: - response = await async_client.sessions.public_share.with_raw_response.create( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - public_share = await response.parse() - assert_matches_type(ShareView, public_share, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_streaming_response_create(self, async_client: AsyncBrowserUse) -> None: - async with async_client.sessions.public_share.with_streaming_response.create( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - public_share = await response.parse() - assert_matches_type(ShareView, public_share, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_path_params_create(self, async_client: AsyncBrowserUse) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `session_id` but received ''"): - await async_client.sessions.public_share.with_raw_response.create( - "", - ) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_method_retrieve(self, async_client: AsyncBrowserUse) -> None: - public_share = await async_client.sessions.public_share.retrieve( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) - assert_matches_type(ShareView, public_share, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_raw_response_retrieve(self, async_client: AsyncBrowserUse) -> None: - response = await async_client.sessions.public_share.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" - public_share = await response.parse() - assert_matches_type(ShareView, public_share, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_streaming_response_retrieve(self, async_client: AsyncBrowserUse) -> None: - async with async_client.sessions.public_share.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" - - public_share = await response.parse() - assert_matches_type(ShareView, public_share, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_path_params_retrieve(self, async_client: AsyncBrowserUse) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `session_id` but received ''"): - await async_client.sessions.public_share.with_raw_response.retrieve( - "", - ) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_method_delete(self, async_client: AsyncBrowserUse) -> None: - public_share = await async_client.sessions.public_share.delete( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) - assert public_share is None - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_raw_response_delete(self, async_client: AsyncBrowserUse) -> None: - response = await async_client.sessions.public_share.with_raw_response.delete( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - public_share = await response.parse() - assert public_share is None - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_streaming_response_delete(self, async_client: AsyncBrowserUse) -> None: - async with async_client.sessions.public_share.with_streaming_response.delete( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - public_share = await response.parse() - assert public_share is None - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_path_params_delete(self, async_client: AsyncBrowserUse) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `session_id` but received ''"): - await async_client.sessions.public_share.with_raw_response.delete( - "", - ) diff --git a/tests/api_resources/test_agent_profiles.py b/tests/api_resources/test_agent_profiles.py deleted file mode 100644 index 77ee21c..0000000 --- a/tests/api_resources/test_agent_profiles.py +++ /dev/null @@ -1,487 +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 pytest - -from tests.utils import assert_matches_type -from browser_use_sdk import BrowserUse, AsyncBrowserUse -from browser_use_sdk.types import ( - AgentProfileView, - AgentProfileListResponse, -) - -base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") - - -class TestAgentProfiles: - parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_method_create(self, client: BrowserUse) -> None: - agent_profile = client.agent_profiles.create( - name="x", - ) - assert_matches_type(AgentProfileView, agent_profile, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_method_create_with_all_params(self, client: BrowserUse) -> None: - agent_profile = client.agent_profiles.create( - name="x", - allowed_domains=["string"], - custom_system_prompt_extension="x", - description="x", - flash_mode=True, - highlight_elements=True, - max_agent_steps=1, - thinking=True, - vision=True, - ) - assert_matches_type(AgentProfileView, agent_profile, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_raw_response_create(self, client: BrowserUse) -> None: - response = client.agent_profiles.with_raw_response.create( - name="x", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - agent_profile = response.parse() - assert_matches_type(AgentProfileView, agent_profile, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_streaming_response_create(self, client: BrowserUse) -> None: - with client.agent_profiles.with_streaming_response.create( - name="x", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - agent_profile = response.parse() - assert_matches_type(AgentProfileView, agent_profile, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_method_retrieve(self, client: BrowserUse) -> None: - agent_profile = client.agent_profiles.retrieve( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) - assert_matches_type(AgentProfileView, agent_profile, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_raw_response_retrieve(self, client: BrowserUse) -> None: - response = client.agent_profiles.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" - agent_profile = response.parse() - assert_matches_type(AgentProfileView, agent_profile, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_streaming_response_retrieve(self, client: BrowserUse) -> None: - with client.agent_profiles.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" - - agent_profile = response.parse() - assert_matches_type(AgentProfileView, agent_profile, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_path_params_retrieve(self, client: BrowserUse) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `profile_id` but received ''"): - client.agent_profiles.with_raw_response.retrieve( - "", - ) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_method_update(self, client: BrowserUse) -> None: - agent_profile = client.agent_profiles.update( - profile_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) - assert_matches_type(AgentProfileView, agent_profile, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_method_update_with_all_params(self, client: BrowserUse) -> None: - agent_profile = client.agent_profiles.update( - profile_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - allowed_domains=["string"], - custom_system_prompt_extension="x", - description="x", - flash_mode=True, - highlight_elements=True, - max_agent_steps=1, - name="x", - thinking=True, - vision=True, - ) - assert_matches_type(AgentProfileView, agent_profile, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_raw_response_update(self, client: BrowserUse) -> None: - response = client.agent_profiles.with_raw_response.update( - profile_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - agent_profile = response.parse() - assert_matches_type(AgentProfileView, agent_profile, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_streaming_response_update(self, client: BrowserUse) -> None: - with client.agent_profiles.with_streaming_response.update( - profile_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - agent_profile = response.parse() - assert_matches_type(AgentProfileView, agent_profile, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_path_params_update(self, client: BrowserUse) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `profile_id` but received ''"): - client.agent_profiles.with_raw_response.update( - profile_id="", - ) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_method_list(self, client: BrowserUse) -> None: - agent_profile = client.agent_profiles.list() - assert_matches_type(AgentProfileListResponse, agent_profile, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_method_list_with_all_params(self, client: BrowserUse) -> None: - agent_profile = client.agent_profiles.list( - page_number=1, - page_size=1, - ) - assert_matches_type(AgentProfileListResponse, agent_profile, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_raw_response_list(self, client: BrowserUse) -> None: - response = client.agent_profiles.with_raw_response.list() - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - agent_profile = response.parse() - assert_matches_type(AgentProfileListResponse, agent_profile, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_streaming_response_list(self, client: BrowserUse) -> None: - with client.agent_profiles.with_streaming_response.list() as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - agent_profile = response.parse() - assert_matches_type(AgentProfileListResponse, agent_profile, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_method_delete(self, client: BrowserUse) -> None: - agent_profile = client.agent_profiles.delete( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) - assert agent_profile is None - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_raw_response_delete(self, client: BrowserUse) -> None: - response = client.agent_profiles.with_raw_response.delete( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - agent_profile = response.parse() - assert agent_profile is None - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_streaming_response_delete(self, client: BrowserUse) -> None: - with client.agent_profiles.with_streaming_response.delete( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - agent_profile = response.parse() - assert agent_profile is None - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_path_params_delete(self, client: BrowserUse) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `profile_id` but received ''"): - client.agent_profiles.with_raw_response.delete( - "", - ) - - -class TestAsyncAgentProfiles: - parametrize = pytest.mark.parametrize( - "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] - ) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_method_create(self, async_client: AsyncBrowserUse) -> None: - agent_profile = await async_client.agent_profiles.create( - name="x", - ) - assert_matches_type(AgentProfileView, agent_profile, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_method_create_with_all_params(self, async_client: AsyncBrowserUse) -> None: - agent_profile = await async_client.agent_profiles.create( - name="x", - allowed_domains=["string"], - custom_system_prompt_extension="x", - description="x", - flash_mode=True, - highlight_elements=True, - max_agent_steps=1, - thinking=True, - vision=True, - ) - assert_matches_type(AgentProfileView, agent_profile, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_raw_response_create(self, async_client: AsyncBrowserUse) -> None: - response = await async_client.agent_profiles.with_raw_response.create( - name="x", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - agent_profile = await response.parse() - assert_matches_type(AgentProfileView, agent_profile, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_streaming_response_create(self, async_client: AsyncBrowserUse) -> None: - async with async_client.agent_profiles.with_streaming_response.create( - name="x", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - agent_profile = await response.parse() - assert_matches_type(AgentProfileView, agent_profile, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_method_retrieve(self, async_client: AsyncBrowserUse) -> None: - agent_profile = await async_client.agent_profiles.retrieve( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) - assert_matches_type(AgentProfileView, agent_profile, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_raw_response_retrieve(self, async_client: AsyncBrowserUse) -> None: - response = await async_client.agent_profiles.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" - agent_profile = await response.parse() - assert_matches_type(AgentProfileView, agent_profile, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_streaming_response_retrieve(self, async_client: AsyncBrowserUse) -> None: - async with async_client.agent_profiles.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" - - agent_profile = await response.parse() - assert_matches_type(AgentProfileView, agent_profile, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_path_params_retrieve(self, async_client: AsyncBrowserUse) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `profile_id` but received ''"): - await async_client.agent_profiles.with_raw_response.retrieve( - "", - ) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_method_update(self, async_client: AsyncBrowserUse) -> None: - agent_profile = await async_client.agent_profiles.update( - profile_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) - assert_matches_type(AgentProfileView, agent_profile, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_method_update_with_all_params(self, async_client: AsyncBrowserUse) -> None: - agent_profile = await async_client.agent_profiles.update( - profile_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - allowed_domains=["string"], - custom_system_prompt_extension="x", - description="x", - flash_mode=True, - highlight_elements=True, - max_agent_steps=1, - name="x", - thinking=True, - vision=True, - ) - assert_matches_type(AgentProfileView, agent_profile, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_raw_response_update(self, async_client: AsyncBrowserUse) -> None: - response = await async_client.agent_profiles.with_raw_response.update( - profile_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - agent_profile = await response.parse() - assert_matches_type(AgentProfileView, agent_profile, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_streaming_response_update(self, async_client: AsyncBrowserUse) -> None: - async with async_client.agent_profiles.with_streaming_response.update( - profile_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - agent_profile = await response.parse() - assert_matches_type(AgentProfileView, agent_profile, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_path_params_update(self, async_client: AsyncBrowserUse) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `profile_id` but received ''"): - await async_client.agent_profiles.with_raw_response.update( - profile_id="", - ) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_method_list(self, async_client: AsyncBrowserUse) -> None: - agent_profile = await async_client.agent_profiles.list() - assert_matches_type(AgentProfileListResponse, agent_profile, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_method_list_with_all_params(self, async_client: AsyncBrowserUse) -> None: - agent_profile = await async_client.agent_profiles.list( - page_number=1, - page_size=1, - ) - assert_matches_type(AgentProfileListResponse, agent_profile, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_raw_response_list(self, async_client: AsyncBrowserUse) -> None: - response = await async_client.agent_profiles.with_raw_response.list() - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - agent_profile = await response.parse() - assert_matches_type(AgentProfileListResponse, agent_profile, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_streaming_response_list(self, async_client: AsyncBrowserUse) -> None: - async with async_client.agent_profiles.with_streaming_response.list() as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - agent_profile = await response.parse() - assert_matches_type(AgentProfileListResponse, agent_profile, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_method_delete(self, async_client: AsyncBrowserUse) -> None: - agent_profile = await async_client.agent_profiles.delete( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) - assert agent_profile is None - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_raw_response_delete(self, async_client: AsyncBrowserUse) -> None: - response = await async_client.agent_profiles.with_raw_response.delete( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - agent_profile = await response.parse() - assert agent_profile is None - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_streaming_response_delete(self, async_client: AsyncBrowserUse) -> None: - async with async_client.agent_profiles.with_streaming_response.delete( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - agent_profile = await response.parse() - assert agent_profile is None - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_path_params_delete(self, async_client: AsyncBrowserUse) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `profile_id` but received ''"): - await async_client.agent_profiles.with_raw_response.delete( - "", - ) diff --git a/tests/api_resources/test_browser_profiles.py b/tests/api_resources/test_browser_profiles.py deleted file mode 100644 index d10b16a..0000000 --- a/tests/api_resources/test_browser_profiles.py +++ /dev/null @@ -1,491 +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 pytest - -from tests.utils import assert_matches_type -from browser_use_sdk import BrowserUse, AsyncBrowserUse -from browser_use_sdk.types import ( - BrowserProfileView, - BrowserProfileListResponse, -) - -base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") - - -class TestBrowserProfiles: - parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_method_create(self, client: BrowserUse) -> None: - browser_profile = client.browser_profiles.create( - name="x", - ) - assert_matches_type(BrowserProfileView, browser_profile, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_method_create_with_all_params(self, client: BrowserUse) -> None: - browser_profile = client.browser_profiles.create( - name="x", - ad_blocker=True, - browser_viewport_height=100, - browser_viewport_width=100, - description="x", - is_mobile=True, - persist=True, - proxy=True, - proxy_country_code="us", - store_cache=True, - ) - assert_matches_type(BrowserProfileView, browser_profile, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_raw_response_create(self, client: BrowserUse) -> None: - response = client.browser_profiles.with_raw_response.create( - name="x", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - browser_profile = response.parse() - assert_matches_type(BrowserProfileView, browser_profile, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_streaming_response_create(self, client: BrowserUse) -> None: - with client.browser_profiles.with_streaming_response.create( - name="x", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - browser_profile = response.parse() - assert_matches_type(BrowserProfileView, browser_profile, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_method_retrieve(self, client: BrowserUse) -> None: - browser_profile = client.browser_profiles.retrieve( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) - assert_matches_type(BrowserProfileView, browser_profile, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_raw_response_retrieve(self, client: BrowserUse) -> None: - response = client.browser_profiles.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" - browser_profile = response.parse() - assert_matches_type(BrowserProfileView, browser_profile, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_streaming_response_retrieve(self, client: BrowserUse) -> None: - with client.browser_profiles.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" - - browser_profile = response.parse() - assert_matches_type(BrowserProfileView, browser_profile, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_path_params_retrieve(self, client: BrowserUse) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `profile_id` but received ''"): - client.browser_profiles.with_raw_response.retrieve( - "", - ) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_method_update(self, client: BrowserUse) -> None: - browser_profile = client.browser_profiles.update( - profile_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) - assert_matches_type(BrowserProfileView, browser_profile, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_method_update_with_all_params(self, client: BrowserUse) -> None: - browser_profile = client.browser_profiles.update( - profile_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ad_blocker=True, - browser_viewport_height=100, - browser_viewport_width=100, - description="x", - is_mobile=True, - name="x", - persist=True, - proxy=True, - proxy_country_code="us", - store_cache=True, - ) - assert_matches_type(BrowserProfileView, browser_profile, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_raw_response_update(self, client: BrowserUse) -> None: - response = client.browser_profiles.with_raw_response.update( - profile_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - browser_profile = response.parse() - assert_matches_type(BrowserProfileView, browser_profile, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_streaming_response_update(self, client: BrowserUse) -> None: - with client.browser_profiles.with_streaming_response.update( - profile_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - browser_profile = response.parse() - assert_matches_type(BrowserProfileView, browser_profile, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_path_params_update(self, client: BrowserUse) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `profile_id` but received ''"): - client.browser_profiles.with_raw_response.update( - profile_id="", - ) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_method_list(self, client: BrowserUse) -> None: - browser_profile = client.browser_profiles.list() - assert_matches_type(BrowserProfileListResponse, browser_profile, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_method_list_with_all_params(self, client: BrowserUse) -> None: - browser_profile = client.browser_profiles.list( - page_number=1, - page_size=1, - ) - assert_matches_type(BrowserProfileListResponse, browser_profile, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_raw_response_list(self, client: BrowserUse) -> None: - response = client.browser_profiles.with_raw_response.list() - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - browser_profile = response.parse() - assert_matches_type(BrowserProfileListResponse, browser_profile, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_streaming_response_list(self, client: BrowserUse) -> None: - with client.browser_profiles.with_streaming_response.list() as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - browser_profile = response.parse() - assert_matches_type(BrowserProfileListResponse, browser_profile, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_method_delete(self, client: BrowserUse) -> None: - browser_profile = client.browser_profiles.delete( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) - assert browser_profile is None - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_raw_response_delete(self, client: BrowserUse) -> None: - response = client.browser_profiles.with_raw_response.delete( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - browser_profile = response.parse() - assert browser_profile is None - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_streaming_response_delete(self, client: BrowserUse) -> None: - with client.browser_profiles.with_streaming_response.delete( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - browser_profile = response.parse() - assert browser_profile is None - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_path_params_delete(self, client: BrowserUse) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `profile_id` but received ''"): - client.browser_profiles.with_raw_response.delete( - "", - ) - - -class TestAsyncBrowserProfiles: - parametrize = pytest.mark.parametrize( - "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] - ) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_method_create(self, async_client: AsyncBrowserUse) -> None: - browser_profile = await async_client.browser_profiles.create( - name="x", - ) - assert_matches_type(BrowserProfileView, browser_profile, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_method_create_with_all_params(self, async_client: AsyncBrowserUse) -> None: - browser_profile = await async_client.browser_profiles.create( - name="x", - ad_blocker=True, - browser_viewport_height=100, - browser_viewport_width=100, - description="x", - is_mobile=True, - persist=True, - proxy=True, - proxy_country_code="us", - store_cache=True, - ) - assert_matches_type(BrowserProfileView, browser_profile, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_raw_response_create(self, async_client: AsyncBrowserUse) -> None: - response = await async_client.browser_profiles.with_raw_response.create( - name="x", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - browser_profile = await response.parse() - assert_matches_type(BrowserProfileView, browser_profile, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_streaming_response_create(self, async_client: AsyncBrowserUse) -> None: - async with async_client.browser_profiles.with_streaming_response.create( - name="x", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - browser_profile = await response.parse() - assert_matches_type(BrowserProfileView, browser_profile, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_method_retrieve(self, async_client: AsyncBrowserUse) -> None: - browser_profile = await async_client.browser_profiles.retrieve( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) - assert_matches_type(BrowserProfileView, browser_profile, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_raw_response_retrieve(self, async_client: AsyncBrowserUse) -> None: - response = await async_client.browser_profiles.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" - browser_profile = await response.parse() - assert_matches_type(BrowserProfileView, browser_profile, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_streaming_response_retrieve(self, async_client: AsyncBrowserUse) -> None: - async with async_client.browser_profiles.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" - - browser_profile = await response.parse() - assert_matches_type(BrowserProfileView, browser_profile, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_path_params_retrieve(self, async_client: AsyncBrowserUse) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `profile_id` but received ''"): - await async_client.browser_profiles.with_raw_response.retrieve( - "", - ) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_method_update(self, async_client: AsyncBrowserUse) -> None: - browser_profile = await async_client.browser_profiles.update( - profile_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) - assert_matches_type(BrowserProfileView, browser_profile, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_method_update_with_all_params(self, async_client: AsyncBrowserUse) -> None: - browser_profile = await async_client.browser_profiles.update( - profile_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ad_blocker=True, - browser_viewport_height=100, - browser_viewport_width=100, - description="x", - is_mobile=True, - name="x", - persist=True, - proxy=True, - proxy_country_code="us", - store_cache=True, - ) - assert_matches_type(BrowserProfileView, browser_profile, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_raw_response_update(self, async_client: AsyncBrowserUse) -> None: - response = await async_client.browser_profiles.with_raw_response.update( - profile_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - browser_profile = await response.parse() - assert_matches_type(BrowserProfileView, browser_profile, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_streaming_response_update(self, async_client: AsyncBrowserUse) -> None: - async with async_client.browser_profiles.with_streaming_response.update( - profile_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - browser_profile = await response.parse() - assert_matches_type(BrowserProfileView, browser_profile, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_path_params_update(self, async_client: AsyncBrowserUse) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `profile_id` but received ''"): - await async_client.browser_profiles.with_raw_response.update( - profile_id="", - ) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_method_list(self, async_client: AsyncBrowserUse) -> None: - browser_profile = await async_client.browser_profiles.list() - assert_matches_type(BrowserProfileListResponse, browser_profile, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_method_list_with_all_params(self, async_client: AsyncBrowserUse) -> None: - browser_profile = await async_client.browser_profiles.list( - page_number=1, - page_size=1, - ) - assert_matches_type(BrowserProfileListResponse, browser_profile, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_raw_response_list(self, async_client: AsyncBrowserUse) -> None: - response = await async_client.browser_profiles.with_raw_response.list() - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - browser_profile = await response.parse() - assert_matches_type(BrowserProfileListResponse, browser_profile, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_streaming_response_list(self, async_client: AsyncBrowserUse) -> None: - async with async_client.browser_profiles.with_streaming_response.list() as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - browser_profile = await response.parse() - assert_matches_type(BrowserProfileListResponse, browser_profile, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_method_delete(self, async_client: AsyncBrowserUse) -> None: - browser_profile = await async_client.browser_profiles.delete( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) - assert browser_profile is None - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_raw_response_delete(self, async_client: AsyncBrowserUse) -> None: - response = await async_client.browser_profiles.with_raw_response.delete( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - browser_profile = await response.parse() - assert browser_profile is None - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_streaming_response_delete(self, async_client: AsyncBrowserUse) -> None: - async with async_client.browser_profiles.with_streaming_response.delete( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - browser_profile = await response.parse() - assert browser_profile is None - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_path_params_delete(self, async_client: AsyncBrowserUse) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `profile_id` but received ''"): - await async_client.browser_profiles.with_raw_response.delete( - "", - ) diff --git a/tests/api_resources/test_sessions.py b/tests/api_resources/test_sessions.py deleted file mode 100644 index 0751000..0000000 --- a/tests/api_resources/test_sessions.py +++ /dev/null @@ -1,363 +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 pytest - -from tests.utils import assert_matches_type -from browser_use_sdk import BrowserUse, AsyncBrowserUse -from browser_use_sdk.types import ( - SessionView, - 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"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_method_retrieve(self, client: BrowserUse) -> None: - session = client.sessions.retrieve( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) - assert_matches_type(SessionView, session, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_raw_response_retrieve(self, client: BrowserUse) -> None: - response = client.sessions.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" - session = response.parse() - assert_matches_type(SessionView, session, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_streaming_response_retrieve(self, client: BrowserUse) -> None: - with client.sessions.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" - - session = response.parse() - assert_matches_type(SessionView, session, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_path_params_retrieve(self, client: BrowserUse) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `session_id` but received ''"): - client.sessions.with_raw_response.retrieve( - "", - ) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_method_update(self, client: BrowserUse) -> None: - session = client.sessions.update( - session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - action="stop", - ) - assert_matches_type(SessionView, session, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_raw_response_update(self, client: BrowserUse) -> None: - response = client.sessions.with_raw_response.update( - session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - action="stop", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - session = response.parse() - assert_matches_type(SessionView, session, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_streaming_response_update(self, client: BrowserUse) -> None: - with client.sessions.with_streaming_response.update( - session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - action="stop", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - session = response.parse() - assert_matches_type(SessionView, session, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_path_params_update(self, client: BrowserUse) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `session_id` but received ''"): - client.sessions.with_raw_response.update( - session_id="", - action="stop", - ) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_method_list(self, client: BrowserUse) -> None: - session = client.sessions.list() - assert_matches_type(SessionListResponse, session, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_method_list_with_all_params(self, client: BrowserUse) -> None: - session = client.sessions.list( - filter_by="active", - page_number=1, - page_size=1, - ) - assert_matches_type(SessionListResponse, session, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_raw_response_list(self, client: BrowserUse) -> 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"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_streaming_response_list(self, client: BrowserUse) -> 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 - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_method_delete(self, client: BrowserUse) -> None: - session = client.sessions.delete( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) - assert session is None - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_raw_response_delete(self, client: BrowserUse) -> None: - response = client.sessions.with_raw_response.delete( - "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 session is None - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_streaming_response_delete(self, client: BrowserUse) -> None: - with client.sessions.with_streaming_response.delete( - "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 session is None - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_path_params_delete(self, client: BrowserUse) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `session_id` but received ''"): - client.sessions.with_raw_response.delete( - "", - ) - - -class TestAsyncSessions: - parametrize = pytest.mark.parametrize( - "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] - ) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_method_retrieve(self, async_client: AsyncBrowserUse) -> None: - session = await async_client.sessions.retrieve( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) - assert_matches_type(SessionView, session, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_raw_response_retrieve(self, async_client: AsyncBrowserUse) -> None: - response = await async_client.sessions.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" - session = await response.parse() - assert_matches_type(SessionView, session, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_streaming_response_retrieve(self, async_client: AsyncBrowserUse) -> None: - async with async_client.sessions.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" - - session = await response.parse() - assert_matches_type(SessionView, session, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_path_params_retrieve(self, async_client: AsyncBrowserUse) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `session_id` but received ''"): - await async_client.sessions.with_raw_response.retrieve( - "", - ) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_method_update(self, async_client: AsyncBrowserUse) -> None: - session = await async_client.sessions.update( - session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - action="stop", - ) - assert_matches_type(SessionView, session, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_raw_response_update(self, async_client: AsyncBrowserUse) -> None: - response = await async_client.sessions.with_raw_response.update( - session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - action="stop", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - session = await response.parse() - assert_matches_type(SessionView, session, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_streaming_response_update(self, async_client: AsyncBrowserUse) -> None: - async with async_client.sessions.with_streaming_response.update( - session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - action="stop", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - session = await response.parse() - assert_matches_type(SessionView, session, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_path_params_update(self, async_client: AsyncBrowserUse) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `session_id` but received ''"): - await async_client.sessions.with_raw_response.update( - session_id="", - action="stop", - ) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_method_list(self, async_client: AsyncBrowserUse) -> None: - session = await async_client.sessions.list() - assert_matches_type(SessionListResponse, session, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_method_list_with_all_params(self, async_client: AsyncBrowserUse) -> None: - session = await async_client.sessions.list( - filter_by="active", - page_number=1, - page_size=1, - ) - assert_matches_type(SessionListResponse, session, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_raw_response_list(self, async_client: AsyncBrowserUse) -> 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"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_streaming_response_list(self, async_client: AsyncBrowserUse) -> 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 - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_method_delete(self, async_client: AsyncBrowserUse) -> None: - session = await async_client.sessions.delete( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) - assert session is None - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_raw_response_delete(self, async_client: AsyncBrowserUse) -> None: - response = await async_client.sessions.with_raw_response.delete( - "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 session is None - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_streaming_response_delete(self, async_client: AsyncBrowserUse) -> None: - async with async_client.sessions.with_streaming_response.delete( - "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 session is None - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_path_params_delete(self, async_client: AsyncBrowserUse) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `session_id` but received ''"): - await async_client.sessions.with_raw_response.delete( - "", - ) diff --git a/tests/api_resources/test_tasks.py b/tests/api_resources/test_tasks.py deleted file mode 100644 index d296a63..0000000 --- a/tests/api_resources/test_tasks.py +++ /dev/null @@ -1,692 +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 pytest - -from tests.utils import assert_matches_type -from browser_use_sdk import BrowserUse, AsyncBrowserUse -from browser_use_sdk.types import ( - TaskView, - TaskListResponse, - TaskCreateResponse, - TaskGetLogsResponse, - TaskGetOutputFileResponse, - TaskGetUserUploadedFileResponse, -) -from browser_use_sdk._utils import parse_datetime - -base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") - - -class TestTasks: - parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_method_create(self, client: BrowserUse) -> None: - task = client.tasks.create( - task="x", - ) - assert_matches_type(TaskCreateResponse, task, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_method_create_with_all_params(self, client: BrowserUse) -> None: - task = client.tasks.create( - task="x", - agent_settings={ - "llm": "gpt-4.1", - "profile_id": "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - "start_url": "startUrl", - }, - browser_settings={ - "profile_id": "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - "session_id": "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - }, - included_file_names=["string"], - metadata={"foo": "string"}, - secrets={"foo": "string"}, - structured_output_json="structuredOutputJson", - ) - assert_matches_type(TaskCreateResponse, task, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_raw_response_create(self, client: BrowserUse) -> None: - response = client.tasks.with_raw_response.create( - task="x", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - task = response.parse() - assert_matches_type(TaskCreateResponse, task, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_streaming_response_create(self, client: BrowserUse) -> None: - with client.tasks.with_streaming_response.create( - task="x", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - task = response.parse() - assert_matches_type(TaskCreateResponse, task, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_method_retrieve(self, client: BrowserUse) -> None: - task = client.tasks.retrieve( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) - assert_matches_type(TaskView, task, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_raw_response_retrieve(self, client: BrowserUse) -> None: - response = client.tasks.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" - task = response.parse() - assert_matches_type(TaskView, task, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_streaming_response_retrieve(self, client: BrowserUse) -> None: - with client.tasks.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" - - task = response.parse() - assert_matches_type(TaskView, task, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_path_params_retrieve(self, client: BrowserUse) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `task_id` but received ''"): - client.tasks.with_raw_response.retrieve( - "", - ) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_method_update(self, client: BrowserUse) -> None: - task = client.tasks.update( - task_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - action="stop", - ) - assert_matches_type(TaskView, task, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_raw_response_update(self, client: BrowserUse) -> None: - response = client.tasks.with_raw_response.update( - task_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - action="stop", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - task = response.parse() - assert_matches_type(TaskView, task, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_streaming_response_update(self, client: BrowserUse) -> None: - with client.tasks.with_streaming_response.update( - task_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - action="stop", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - task = response.parse() - assert_matches_type(TaskView, task, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_path_params_update(self, client: BrowserUse) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `task_id` but received ''"): - client.tasks.with_raw_response.update( - task_id="", - action="stop", - ) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_method_list(self, client: BrowserUse) -> None: - task = client.tasks.list() - assert_matches_type(TaskListResponse, task, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_method_list_with_all_params(self, client: BrowserUse) -> None: - task = client.tasks.list( - after=parse_datetime("2019-12-27T18:11:19.117Z"), - before=parse_datetime("2019-12-27T18:11:19.117Z"), - filter_by="started", - page_number=1, - page_size=1, - session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) - assert_matches_type(TaskListResponse, task, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_raw_response_list(self, client: BrowserUse) -> None: - response = client.tasks.with_raw_response.list() - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - task = response.parse() - assert_matches_type(TaskListResponse, task, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_streaming_response_list(self, client: BrowserUse) -> None: - with client.tasks.with_streaming_response.list() as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - task = response.parse() - assert_matches_type(TaskListResponse, task, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_method_get_logs(self, client: BrowserUse) -> None: - task = client.tasks.get_logs( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) - assert_matches_type(TaskGetLogsResponse, task, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_raw_response_get_logs(self, client: BrowserUse) -> None: - response = client.tasks.with_raw_response.get_logs( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - task = response.parse() - assert_matches_type(TaskGetLogsResponse, task, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_streaming_response_get_logs(self, client: BrowserUse) -> None: - with client.tasks.with_streaming_response.get_logs( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - task = response.parse() - assert_matches_type(TaskGetLogsResponse, task, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_path_params_get_logs(self, client: BrowserUse) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `task_id` but received ''"): - client.tasks.with_raw_response.get_logs( - "", - ) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_method_get_output_file(self, client: BrowserUse) -> None: - task = client.tasks.get_output_file( - file_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - task_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) - assert_matches_type(TaskGetOutputFileResponse, task, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_raw_response_get_output_file(self, client: BrowserUse) -> None: - response = client.tasks.with_raw_response.get_output_file( - file_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - task_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - task = response.parse() - assert_matches_type(TaskGetOutputFileResponse, task, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_streaming_response_get_output_file(self, client: BrowserUse) -> None: - with client.tasks.with_streaming_response.get_output_file( - file_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - task_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - task = response.parse() - assert_matches_type(TaskGetOutputFileResponse, task, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_path_params_get_output_file(self, client: BrowserUse) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `task_id` but received ''"): - client.tasks.with_raw_response.get_output_file( - file_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - task_id="", - ) - - with pytest.raises(ValueError, match=r"Expected a non-empty value for `file_id` but received ''"): - client.tasks.with_raw_response.get_output_file( - file_id="", - task_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_method_get_user_uploaded_file(self, client: BrowserUse) -> None: - task = client.tasks.get_user_uploaded_file( - file_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - task_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) - assert_matches_type(TaskGetUserUploadedFileResponse, task, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_raw_response_get_user_uploaded_file(self, client: BrowserUse) -> None: - response = client.tasks.with_raw_response.get_user_uploaded_file( - file_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - task_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - task = response.parse() - assert_matches_type(TaskGetUserUploadedFileResponse, task, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_streaming_response_get_user_uploaded_file(self, client: BrowserUse) -> None: - with client.tasks.with_streaming_response.get_user_uploaded_file( - file_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - task_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - task = response.parse() - assert_matches_type(TaskGetUserUploadedFileResponse, task, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_path_params_get_user_uploaded_file(self, client: BrowserUse) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `task_id` but received ''"): - client.tasks.with_raw_response.get_user_uploaded_file( - file_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - task_id="", - ) - - with pytest.raises(ValueError, match=r"Expected a non-empty value for `file_id` but received ''"): - client.tasks.with_raw_response.get_user_uploaded_file( - file_id="", - task_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) - - -class TestAsyncTasks: - parametrize = pytest.mark.parametrize( - "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] - ) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_method_create(self, async_client: AsyncBrowserUse) -> None: - task = await async_client.tasks.create( - task="x", - ) - assert_matches_type(TaskCreateResponse, task, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_method_create_with_all_params(self, async_client: AsyncBrowserUse) -> None: - task = await async_client.tasks.create( - task="x", - agent_settings={ - "llm": "gpt-4.1", - "profile_id": "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - "start_url": "startUrl", - }, - browser_settings={ - "profile_id": "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - "session_id": "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - }, - included_file_names=["string"], - metadata={"foo": "string"}, - secrets={"foo": "string"}, - structured_output_json="structuredOutputJson", - ) - assert_matches_type(TaskCreateResponse, task, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_raw_response_create(self, async_client: AsyncBrowserUse) -> None: - response = await async_client.tasks.with_raw_response.create( - task="x", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - task = await response.parse() - assert_matches_type(TaskCreateResponse, task, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_streaming_response_create(self, async_client: AsyncBrowserUse) -> None: - async with async_client.tasks.with_streaming_response.create( - task="x", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - task = await response.parse() - assert_matches_type(TaskCreateResponse, task, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_method_retrieve(self, async_client: AsyncBrowserUse) -> None: - task = await async_client.tasks.retrieve( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) - assert_matches_type(TaskView, task, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_raw_response_retrieve(self, async_client: AsyncBrowserUse) -> None: - response = await async_client.tasks.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" - task = await response.parse() - assert_matches_type(TaskView, task, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_streaming_response_retrieve(self, async_client: AsyncBrowserUse) -> None: - async with async_client.tasks.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" - - task = await response.parse() - assert_matches_type(TaskView, task, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_path_params_retrieve(self, async_client: AsyncBrowserUse) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `task_id` but received ''"): - await async_client.tasks.with_raw_response.retrieve( - "", - ) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_method_update(self, async_client: AsyncBrowserUse) -> None: - task = await async_client.tasks.update( - task_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - action="stop", - ) - assert_matches_type(TaskView, task, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_raw_response_update(self, async_client: AsyncBrowserUse) -> None: - response = await async_client.tasks.with_raw_response.update( - task_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - action="stop", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - task = await response.parse() - assert_matches_type(TaskView, task, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_streaming_response_update(self, async_client: AsyncBrowserUse) -> None: - async with async_client.tasks.with_streaming_response.update( - task_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - action="stop", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - task = await response.parse() - assert_matches_type(TaskView, task, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_path_params_update(self, async_client: AsyncBrowserUse) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `task_id` but received ''"): - await async_client.tasks.with_raw_response.update( - task_id="", - action="stop", - ) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_method_list(self, async_client: AsyncBrowserUse) -> None: - task = await async_client.tasks.list() - assert_matches_type(TaskListResponse, task, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_method_list_with_all_params(self, async_client: AsyncBrowserUse) -> None: - task = await async_client.tasks.list( - after=parse_datetime("2019-12-27T18:11:19.117Z"), - before=parse_datetime("2019-12-27T18:11:19.117Z"), - filter_by="started", - page_number=1, - page_size=1, - session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) - assert_matches_type(TaskListResponse, task, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_raw_response_list(self, async_client: AsyncBrowserUse) -> None: - response = await async_client.tasks.with_raw_response.list() - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - task = await response.parse() - assert_matches_type(TaskListResponse, task, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_streaming_response_list(self, async_client: AsyncBrowserUse) -> None: - async with async_client.tasks.with_streaming_response.list() as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - task = await response.parse() - assert_matches_type(TaskListResponse, task, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_method_get_logs(self, async_client: AsyncBrowserUse) -> None: - task = await async_client.tasks.get_logs( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) - assert_matches_type(TaskGetLogsResponse, task, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_raw_response_get_logs(self, async_client: AsyncBrowserUse) -> None: - response = await async_client.tasks.with_raw_response.get_logs( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - task = await response.parse() - assert_matches_type(TaskGetLogsResponse, task, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_streaming_response_get_logs(self, async_client: AsyncBrowserUse) -> None: - async with async_client.tasks.with_streaming_response.get_logs( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - task = await response.parse() - assert_matches_type(TaskGetLogsResponse, task, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_path_params_get_logs(self, async_client: AsyncBrowserUse) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `task_id` but received ''"): - await async_client.tasks.with_raw_response.get_logs( - "", - ) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_method_get_output_file(self, async_client: AsyncBrowserUse) -> None: - task = await async_client.tasks.get_output_file( - file_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - task_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) - assert_matches_type(TaskGetOutputFileResponse, task, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_raw_response_get_output_file(self, async_client: AsyncBrowserUse) -> None: - response = await async_client.tasks.with_raw_response.get_output_file( - file_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - task_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - task = await response.parse() - assert_matches_type(TaskGetOutputFileResponse, task, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_streaming_response_get_output_file(self, async_client: AsyncBrowserUse) -> None: - async with async_client.tasks.with_streaming_response.get_output_file( - file_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - task_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - task = await response.parse() - assert_matches_type(TaskGetOutputFileResponse, task, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_path_params_get_output_file(self, async_client: AsyncBrowserUse) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `task_id` but received ''"): - await async_client.tasks.with_raw_response.get_output_file( - file_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - task_id="", - ) - - with pytest.raises(ValueError, match=r"Expected a non-empty value for `file_id` but received ''"): - await async_client.tasks.with_raw_response.get_output_file( - file_id="", - task_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_method_get_user_uploaded_file(self, async_client: AsyncBrowserUse) -> None: - task = await async_client.tasks.get_user_uploaded_file( - file_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - task_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) - assert_matches_type(TaskGetUserUploadedFileResponse, task, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_raw_response_get_user_uploaded_file(self, async_client: AsyncBrowserUse) -> None: - response = await async_client.tasks.with_raw_response.get_user_uploaded_file( - file_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - task_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - task = await response.parse() - assert_matches_type(TaskGetUserUploadedFileResponse, task, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_streaming_response_get_user_uploaded_file(self, async_client: AsyncBrowserUse) -> None: - async with async_client.tasks.with_streaming_response.get_user_uploaded_file( - file_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - task_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - task = await response.parse() - assert_matches_type(TaskGetUserUploadedFileResponse, task, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_path_params_get_user_uploaded_file(self, async_client: AsyncBrowserUse) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `task_id` but received ''"): - await async_client.tasks.with_raw_response.get_user_uploaded_file( - file_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - task_id="", - ) - - with pytest.raises(ValueError, match=r"Expected a non-empty value for `file_id` but received ''"): - await async_client.tasks.with_raw_response.get_user_uploaded_file( - file_id="", - task_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) diff --git a/tests/api_resources/users/__init__.py b/tests/api_resources/users/__init__.py deleted file mode 100644 index fd8019a..0000000 --- a/tests/api_resources/users/__init__.py +++ /dev/null @@ -1 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. diff --git a/tests/api_resources/users/me/__init__.py b/tests/api_resources/users/me/__init__.py deleted file mode 100644 index fd8019a..0000000 --- a/tests/api_resources/users/me/__init__.py +++ /dev/null @@ -1 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. diff --git a/tests/api_resources/users/me/test_files.py b/tests/api_resources/users/me/test_files.py deleted file mode 100644 index 974f05a..0000000 --- a/tests/api_resources/users/me/test_files.py +++ /dev/null @@ -1,104 +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 pytest - -from tests.utils import assert_matches_type -from browser_use_sdk import BrowserUse, AsyncBrowserUse -from browser_use_sdk.types.users.me import FileCreatePresignedURLResponse - -base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") - - -class TestFiles: - parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_method_create_presigned_url(self, client: BrowserUse) -> None: - file = client.users.me.files.create_presigned_url( - content_type="image/jpg", - file_name="x", - size_bytes=1, - ) - assert_matches_type(FileCreatePresignedURLResponse, file, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_raw_response_create_presigned_url(self, client: BrowserUse) -> None: - response = client.users.me.files.with_raw_response.create_presigned_url( - content_type="image/jpg", - file_name="x", - size_bytes=1, - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - file = response.parse() - assert_matches_type(FileCreatePresignedURLResponse, file, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_streaming_response_create_presigned_url(self, client: BrowserUse) -> None: - with client.users.me.files.with_streaming_response.create_presigned_url( - content_type="image/jpg", - file_name="x", - size_bytes=1, - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - file = response.parse() - assert_matches_type(FileCreatePresignedURLResponse, file, path=["response"]) - - assert cast(Any, response.is_closed) is True - - -class TestAsyncFiles: - parametrize = pytest.mark.parametrize( - "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] - ) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_method_create_presigned_url(self, async_client: AsyncBrowserUse) -> None: - file = await async_client.users.me.files.create_presigned_url( - content_type="image/jpg", - file_name="x", - size_bytes=1, - ) - assert_matches_type(FileCreatePresignedURLResponse, file, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_raw_response_create_presigned_url(self, async_client: AsyncBrowserUse) -> None: - response = await async_client.users.me.files.with_raw_response.create_presigned_url( - content_type="image/jpg", - file_name="x", - size_bytes=1, - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - file = await response.parse() - assert_matches_type(FileCreatePresignedURLResponse, file, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_streaming_response_create_presigned_url(self, async_client: AsyncBrowserUse) -> None: - async with async_client.users.me.files.with_streaming_response.create_presigned_url( - content_type="image/jpg", - file_name="x", - size_bytes=1, - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - file = await response.parse() - assert_matches_type(FileCreatePresignedURLResponse, file, path=["response"]) - - assert cast(Any, response.is_closed) is True diff --git a/tests/api_resources/users/test_me.py b/tests/api_resources/users/test_me.py deleted file mode 100644 index 20e7981..0000000 --- a/tests/api_resources/users/test_me.py +++ /dev/null @@ -1,80 +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 pytest - -from tests.utils import assert_matches_type -from browser_use_sdk import BrowserUse, AsyncBrowserUse -from browser_use_sdk.types.users import MeRetrieveResponse - -base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") - - -class TestMe: - parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_method_retrieve(self, client: BrowserUse) -> None: - me = client.users.me.retrieve() - assert_matches_type(MeRetrieveResponse, me, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_raw_response_retrieve(self, client: BrowserUse) -> None: - response = client.users.me.with_raw_response.retrieve() - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - me = response.parse() - assert_matches_type(MeRetrieveResponse, me, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_streaming_response_retrieve(self, client: BrowserUse) -> None: - with client.users.me.with_streaming_response.retrieve() as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - me = response.parse() - assert_matches_type(MeRetrieveResponse, me, path=["response"]) - - assert cast(Any, response.is_closed) is True - - -class TestAsyncMe: - parametrize = pytest.mark.parametrize( - "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] - ) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_method_retrieve(self, async_client: AsyncBrowserUse) -> None: - me = await async_client.users.me.retrieve() - assert_matches_type(MeRetrieveResponse, me, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_raw_response_retrieve(self, async_client: AsyncBrowserUse) -> None: - response = await async_client.users.me.with_raw_response.retrieve() - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - me = await response.parse() - assert_matches_type(MeRetrieveResponse, me, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_streaming_response_retrieve(self, async_client: AsyncBrowserUse) -> None: - async with async_client.users.me.with_streaming_response.retrieve() as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - me = await response.parse() - assert_matches_type(MeRetrieveResponse, me, path=["response"]) - - assert cast(Any, response.is_closed) is True diff --git a/tests/conftest.py b/tests/conftest.py deleted file mode 100644 index ef9cc12..0000000 --- a/tests/conftest.py +++ /dev/null @@ -1,84 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -import os -import logging -from typing import TYPE_CHECKING, Iterator, AsyncIterator - -import httpx -import pytest -from pytest_asyncio import is_async_test - -from browser_use_sdk import BrowserUse, AsyncBrowserUse, DefaultAioHttpClient -from browser_use_sdk._utils import is_dict - -if TYPE_CHECKING: - from _pytest.fixtures import FixtureRequest # pyright: ignore[reportPrivateImportUsage] - -pytest.register_assert_rewrite("tests.utils") - -logging.getLogger("browser_use_sdk").setLevel(logging.DEBUG) - - -# automatically add `pytest.mark.asyncio()` to all of our async tests -# so we don't have to add that boilerplate everywhere -def pytest_collection_modifyitems(items: list[pytest.Function]) -> None: - pytest_asyncio_tests = (item for item in items if is_async_test(item)) - session_scope_marker = pytest.mark.asyncio(loop_scope="session") - for async_test in pytest_asyncio_tests: - async_test.add_marker(session_scope_marker, append=False) - - # We skip tests that use both the aiohttp client and respx_mock as respx_mock - # doesn't support custom transports. - for item in items: - if "async_client" not in item.fixturenames or "respx_mock" not in item.fixturenames: - continue - - if not hasattr(item, "callspec"): - continue - - async_client_param = item.callspec.params.get("async_client") - if is_dict(async_client_param) and async_client_param.get("http_client") == "aiohttp": - item.add_marker(pytest.mark.skip(reason="aiohttp client is not compatible with respx_mock")) - - -base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") - -api_key = "My API Key" - - -@pytest.fixture(scope="session") -def client(request: FixtureRequest) -> Iterator[BrowserUse]: - strict = getattr(request, "param", True) - if not isinstance(strict, bool): - raise TypeError(f"Unexpected fixture parameter type {type(strict)}, expected {bool}") - - with BrowserUse(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[AsyncBrowserUse]: - 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 AsyncBrowserUse( - base_url=base_url, api_key=api_key, _strict_response_validation=strict, http_client=http_client - ) as client: - yield client diff --git a/tests/custom/test_client.py b/tests/custom/test_client.py new file mode 100644 index 0000000..ab04ce6 --- /dev/null +++ b/tests/custom/test_client.py @@ -0,0 +1,7 @@ +import pytest + + +# Get started with writing tests with pytest at https://docs.pytest.org +@pytest.mark.skip(reason="Unimplemented") +def test_client() -> None: + assert True diff --git a/tests/sample_file.txt b/tests/sample_file.txt deleted file mode 100644 index af5626b..0000000 --- a/tests/sample_file.txt +++ /dev/null @@ -1 +0,0 @@ -Hello, world! diff --git a/tests/test_client.py b/tests/test_client.py deleted file mode 100644 index a3b498a..0000000 --- a/tests/test_client.py +++ /dev/null @@ -1,1736 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -import gc -import os -import sys -import json -import 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 - -import httpx -import pytest -from respx import MockRouter -from pydantic import ValidationError - -from browser_use_sdk import BrowserUse, AsyncBrowserUse, APIResponseValidationError -from browser_use_sdk._types import Omit -from browser_use_sdk._models import BaseModel, FinalRequestOptions -from browser_use_sdk._exceptions import APIStatusError, APITimeoutError, BrowserUseError, APIResponseValidationError -from browser_use_sdk._base_client import ( - DEFAULT_TIMEOUT, - HTTPX_DEFAULT_TIMEOUT, - BaseClient, - DefaultHttpxClient, - DefaultAsyncHttpxClient, - 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: BrowserUse | AsyncBrowserUse) -> int: - transport = client._client._transport - assert isinstance(transport, httpx.HTTPTransport) or isinstance(transport, httpx.AsyncHTTPTransport) - - pool = transport._pool - return len(pool._requests) - - -class TestBrowserUse: - client = BrowserUse(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 = BrowserUse( - 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 = BrowserUse( - 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" - - @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") - - 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. - "browser_use_sdk/_legacy_response.py", - "browser_use_sdk/_response.py", - # pydantic.BaseModel.model_dump || pydantic.BaseModel.dict leak memory for some reason. - "browser_use_sdk/_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 = BrowserUse( - 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 = BrowserUse( - 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 = BrowserUse( - 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 = BrowserUse( - 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: - BrowserUse( - 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 = BrowserUse( - 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 = BrowserUse( - 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 = BrowserUse(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-Browser-Use-API-Key") == api_key - - with pytest.raises(BrowserUseError): - with update_env(**{"BROWSER_USE_API_KEY": Omit()}): - client2 = BrowserUse(base_url=base_url, api_key=None, _strict_response_validation=True) - _ = client2 - - def test_default_query_option(self) -> None: - client = BrowserUse( - base_url=base_url, api_key=api_key, _strict_response_validation=True, default_query={"query_param": "bar"} - ) - request = client._build_request(FinalRequestOptions(method="get", url="/foo")) - url = httpx.URL(request.url) - assert dict(url.params) == {"query_param": "bar"} - - request = client._build_request( - FinalRequestOptions( - method="get", - url="/foo", - params={"foo": "baz", "query_param": "overridden"}, - ) - ) - url = httpx.URL(request.url) - assert dict(url.params) == {"foo": "baz", "query_param": "overridden"} - - 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: BrowserUse) -> None: - request = client._build_request( - FinalRequestOptions.construct( - method="post", - url="/foo", - headers={"Content-Type": "multipart/form-data; boundary=6b7ba517decee4a450543ea6ae821c82"}, - json_data={"array": ["foo", "bar"]}, - files=[("foo.txt", b"hello world")], - ) - ) - - assert request.read().split(b"\r\n") == [ - b"--6b7ba517decee4a450543ea6ae821c82", - b'Content-Disposition: form-data; name="array[]"', - b"", - b"foo", - b"--6b7ba517decee4a450543ea6ae821c82", - b'Content-Disposition: form-data; name="array[]"', - b"", - b"bar", - b"--6b7ba517decee4a450543ea6ae821c82", - b'Content-Disposition: form-data; name="foo.txt"; filename="upload"', - b"Content-Type: application/octet-stream", - b"", - b"hello world", - b"--6b7ba517decee4a450543ea6ae821c82--", - b"", - ] - - @pytest.mark.respx(base_url=base_url) - def test_basic_union_response(self, respx_mock: MockRouter) -> 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 = BrowserUse(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(BROWSER_USE_BASE_URL="http://localhost:5000/from/env"): - client = BrowserUse(api_key=api_key, _strict_response_validation=True) - assert client.base_url == "http://localhost:5000/from/env/" - - @pytest.mark.parametrize( - "client", - [ - BrowserUse( - base_url="http://localhost:5000/custom/path/", api_key=api_key, _strict_response_validation=True - ), - BrowserUse( - 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: BrowserUse) -> 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", - [ - BrowserUse( - base_url="http://localhost:5000/custom/path/", api_key=api_key, _strict_response_validation=True - ), - BrowserUse( - 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: BrowserUse) -> 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", - [ - BrowserUse( - base_url="http://localhost:5000/custom/path/", api_key=api_key, _strict_response_validation=True - ), - BrowserUse( - 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: BrowserUse) -> 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 = BrowserUse(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 = BrowserUse(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"): - BrowserUse( - 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 = BrowserUse(base_url=base_url, api_key=api_key, _strict_response_validation=True) - - with pytest.raises(APIResponseValidationError): - strict_client.get("/foo", cast_to=Model) - - client = BrowserUse(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, "", 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 = BrowserUse(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("browser_use_sdk._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) - @pytest.mark.respx(base_url=base_url) - def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter, client: BrowserUse) -> None: - respx_mock.post("/tasks").mock(side_effect=httpx.TimeoutException("Test timeout error")) - - with pytest.raises(APITimeoutError): - client.tasks.with_streaming_response.create(task="x").__enter__() - - assert _get_open_connections(self.client) == 0 - - @mock.patch("browser_use_sdk._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) - @pytest.mark.respx(base_url=base_url) - def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter, client: BrowserUse) -> None: - respx_mock.post("/tasks").mock(return_value=httpx.Response(500)) - - with pytest.raises(APIStatusError): - client.tasks.with_streaming_response.create(task="x").__enter__() - assert _get_open_connections(self.client) == 0 - - @pytest.mark.parametrize("failures_before_success", [0, 2, 4]) - @mock.patch("browser_use_sdk._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: BrowserUse, - 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("/tasks").mock(side_effect=retry_handler) - - response = client.tasks.with_raw_response.create(task="x") - - 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("browser_use_sdk._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) - @pytest.mark.respx(base_url=base_url) - def test_omit_retry_count_header( - self, client: BrowserUse, 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("/tasks").mock(side_effect=retry_handler) - - response = client.tasks.with_raw_response.create(task="x", 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("browser_use_sdk._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) - @pytest.mark.respx(base_url=base_url) - def test_overwrite_retry_count_header( - self, client: BrowserUse, 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("/tasks").mock(side_effect=retry_handler) - - response = client.tasks.with_raw_response.create(task="x", extra_headers={"x-stainless-retry-count": "42"}) - - assert response.http_request.headers.get("x-stainless-retry-count") == "42" - - def test_proxy_environment_variables(self, monkeypatch: pytest.MonkeyPatch) -> None: - # Test that the proxy environment variables are set correctly - monkeypatch.setenv("HTTPS_PROXY", "https://example.org") - - client = DefaultHttpxClient() - - mounts = tuple(client._mounts.items()) - assert len(mounts) == 1 - assert mounts[0][0].pattern == "https://" - - @pytest.mark.filterwarnings("ignore:.*deprecated.*:DeprecationWarning") - def test_default_client_creation(self) -> None: - # Ensure that the client can be initialized without any exceptions - DefaultHttpxClient( - verify=True, - cert=None, - trust_env=True, - http1=True, - http2=False, - limits=httpx.Limits(max_connections=100, max_keepalive_connections=20), - ) - - @pytest.mark.respx(base_url=base_url) - def test_follow_redirects(self, respx_mock: MockRouter) -> 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 TestAsyncBrowserUse: - client = AsyncBrowserUse(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 = AsyncBrowserUse( - 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 = AsyncBrowserUse( - 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" - - @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") - - 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. - "browser_use_sdk/_legacy_response.py", - "browser_use_sdk/_response.py", - # pydantic.BaseModel.model_dump || pydantic.BaseModel.dict leak memory for some reason. - "browser_use_sdk/_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 = AsyncBrowserUse( - 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 = AsyncBrowserUse( - 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 = AsyncBrowserUse( - 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 = AsyncBrowserUse( - 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: - AsyncBrowserUse( - 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 = AsyncBrowserUse( - 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 = AsyncBrowserUse( - 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 = AsyncBrowserUse(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-Browser-Use-API-Key") == api_key - - with pytest.raises(BrowserUseError): - with update_env(**{"BROWSER_USE_API_KEY": Omit()}): - client2 = AsyncBrowserUse(base_url=base_url, api_key=None, _strict_response_validation=True) - _ = client2 - - def test_default_query_option(self) -> None: - client = AsyncBrowserUse( - base_url=base_url, api_key=api_key, _strict_response_validation=True, default_query={"query_param": "bar"} - ) - request = client._build_request(FinalRequestOptions(method="get", url="/foo")) - url = httpx.URL(request.url) - assert dict(url.params) == {"query_param": "bar"} - - request = client._build_request( - FinalRequestOptions( - method="get", - url="/foo", - params={"foo": "baz", "query_param": "overridden"}, - ) - ) - url = httpx.URL(request.url) - assert dict(url.params) == {"foo": "baz", "query_param": "overridden"} - - 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: AsyncBrowserUse) -> None: - request = async_client._build_request( - FinalRequestOptions.construct( - method="post", - url="/foo", - headers={"Content-Type": "multipart/form-data; boundary=6b7ba517decee4a450543ea6ae821c82"}, - json_data={"array": ["foo", "bar"]}, - files=[("foo.txt", b"hello world")], - ) - ) - - assert request.read().split(b"\r\n") == [ - b"--6b7ba517decee4a450543ea6ae821c82", - b'Content-Disposition: form-data; name="array[]"', - b"", - b"foo", - b"--6b7ba517decee4a450543ea6ae821c82", - b'Content-Disposition: form-data; name="array[]"', - b"", - b"bar", - b"--6b7ba517decee4a450543ea6ae821c82", - b'Content-Disposition: form-data; name="foo.txt"; filename="upload"', - b"Content-Type: application/octet-stream", - b"", - b"hello world", - b"--6b7ba517decee4a450543ea6ae821c82--", - b"", - ] - - @pytest.mark.respx(base_url=base_url) - async def test_basic_union_response(self, respx_mock: MockRouter) -> 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 = AsyncBrowserUse( - 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(BROWSER_USE_BASE_URL="http://localhost:5000/from/env"): - client = AsyncBrowserUse(api_key=api_key, _strict_response_validation=True) - assert client.base_url == "http://localhost:5000/from/env/" - - @pytest.mark.parametrize( - "client", - [ - AsyncBrowserUse( - base_url="http://localhost:5000/custom/path/", api_key=api_key, _strict_response_validation=True - ), - AsyncBrowserUse( - 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: AsyncBrowserUse) -> 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", - [ - AsyncBrowserUse( - base_url="http://localhost:5000/custom/path/", api_key=api_key, _strict_response_validation=True - ), - AsyncBrowserUse( - 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: AsyncBrowserUse) -> 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", - [ - AsyncBrowserUse( - base_url="http://localhost:5000/custom/path/", api_key=api_key, _strict_response_validation=True - ), - AsyncBrowserUse( - 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: AsyncBrowserUse) -> 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 = AsyncBrowserUse(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 = AsyncBrowserUse(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"): - AsyncBrowserUse( - 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 = AsyncBrowserUse(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 = AsyncBrowserUse(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, "", 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 = AsyncBrowserUse(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("browser_use_sdk._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) - @pytest.mark.respx(base_url=base_url) - async def test_retrying_timeout_errors_doesnt_leak( - self, respx_mock: MockRouter, async_client: AsyncBrowserUse - ) -> None: - respx_mock.post("/tasks").mock(side_effect=httpx.TimeoutException("Test timeout error")) - - with pytest.raises(APITimeoutError): - await async_client.tasks.with_streaming_response.create(task="x").__aenter__() - - assert _get_open_connections(self.client) == 0 - - @mock.patch("browser_use_sdk._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) - @pytest.mark.respx(base_url=base_url) - async def test_retrying_status_errors_doesnt_leak( - self, respx_mock: MockRouter, async_client: AsyncBrowserUse - ) -> None: - respx_mock.post("/tasks").mock(return_value=httpx.Response(500)) - - with pytest.raises(APIStatusError): - await async_client.tasks.with_streaming_response.create(task="x").__aenter__() - assert _get_open_connections(self.client) == 0 - - @pytest.mark.parametrize("failures_before_success", [0, 2, 4]) - @mock.patch("browser_use_sdk._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: AsyncBrowserUse, - 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("/tasks").mock(side_effect=retry_handler) - - response = await client.tasks.with_raw_response.create(task="x") - - 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("browser_use_sdk._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: AsyncBrowserUse, 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("/tasks").mock(side_effect=retry_handler) - - response = await client.tasks.with_raw_response.create( - task="x", 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("browser_use_sdk._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: AsyncBrowserUse, 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("/tasks").mock(side_effect=retry_handler) - - response = await client.tasks.with_raw_response.create( - task="x", extra_headers={"x-stainless-retry-count": "42"} - ) - - 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 browser_use_sdk._utils import asyncify - from browser_use_sdk._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_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 - 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" diff --git a/tests/test_deepcopy.py b/tests/test_deepcopy.py deleted file mode 100644 index 852adc3..0000000 --- a/tests/test_deepcopy.py +++ /dev/null @@ -1,58 +0,0 @@ -from browser_use_sdk._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 deleted file mode 100644 index 151f6c6..0000000 --- a/tests/test_extract_files.py +++ /dev/null @@ -1,64 +0,0 @@ -from __future__ import annotations - -from typing import Sequence - -import pytest - -from browser_use_sdk._types import FileTypes -from browser_use_sdk._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 deleted file mode 100644 index f14804f..0000000 --- a/tests/test_files.py +++ /dev/null @@ -1,51 +0,0 @@ -from pathlib import Path - -import anyio -import pytest -from dirty_equals import IsDict, IsList, IsBytes, IsTuple - -from browser_use_sdk._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 deleted file mode 100644 index ee8ad87..0000000 --- a/tests/test_models.py +++ /dev/null @@ -1,963 +0,0 @@ -import json -from typing import TYPE_CHECKING, Any, Dict, List, Union, Optional, cast -from datetime import datetime, timezone -from typing_extensions import Literal, Annotated, TypeAliasType - -import pytest -import pydantic -from pydantic import Field - -from browser_use_sdk._utils import PropertyInfo -from browser_use_sdk._compat import PYDANTIC_V2, parse_obj, model_dump, model_json -from browser_use_sdk._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 m.resource_id is None - assert "resource_id" not in m.model_fields_set - - m = Model.construct(resource_id=None) - assert m.resource_id is None - assert "resource_id" in m.model_fields_set - - m = Model.construct(resource_id="foo") - assert m.resource_id == "foo" - assert "resource_id" in m.model_fields_set - - -def test_to_dict() -> None: - class Model(BaseModel): - foo: Optional[str] = Field(alias="FOO", default=None) - - m = Model(FOO="hello") - assert m.to_dict() == {"FOO": "hello"} - assert m.to_dict(use_api_names=False) == {"foo": "hello"} - - m2 = Model() - assert m2.to_dict() == {} - assert m2.to_dict(exclude_unset=False) == {"FOO": None} - assert m2.to_dict(exclude_unset=False, exclude_none=True) == {} - assert m2.to_dict(exclude_unset=False, exclude_defaults=True) == {} - - m3 = Model(FOO=None) - assert m3.to_dict() == {"FOO": None} - assert m3.to_dict(exclude_none=True) == {} - assert m3.to_dict(exclude_defaults=True) == {} - - class Model2(BaseModel): - created_at: datetime - - time_str = "2024-03-21T11:39:01.275859" - m4 = Model2.construct(created_at=time_str) - assert m4.to_dict(mode="python") == {"created_at": datetime.fromisoformat(time_str)} - assert m4.to_dict(mode="json") == {"created_at": time_str} - - if not PYDANTIC_V2: - 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="round_trip is only supported in Pydantic v2"): - m.model_dump(round_trip=True) - - with pytest.raises(ValueError, match="warnings is only supported in Pydantic v2"): - m.model_dump(warnings=False) - - -def test_compat_method_no_error_for_warnings() -> None: - class Model(BaseModel): - foo: Optional[str] - - m = Model(foo="hello") - assert isinstance(model_dump(m, warnings=False), dict) - - -def test_to_json() -> None: - class Model(BaseModel): - foo: Optional[str] = Field(alias="FOO", default=None) - - m = Model(FOO="hello") - assert json.loads(m.to_json()) == {"FOO": "hello"} - assert json.loads(m.to_json(use_api_names=False)) == {"foo": "hello"} - - if PYDANTIC_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 - - -@pytest.mark.skipif(not PYDANTIC_V2, reason="TypeAliasType is not supported in Pydantic v1") -def test_type_alias_type() -> None: - Alias = TypeAliasType("Alias", str) # pyright: ignore - - class Model(BaseModel): - alias: Alias - union: Union[int, Alias] - - m = construct_type(value={"alias": "foo", "union": "bar"}, type_=Model) - assert isinstance(m, Model) - assert isinstance(m.alias, str) - assert m.alias == "foo" - assert isinstance(m.union, str) - assert m.union == "bar" - - -@pytest.mark.skipif(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) - - -def test_discriminated_union_case() -> None: - class A(BaseModel): - type: Literal["a"] - - data: bool - - class B(BaseModel): - type: Literal["b"] - - data: List[Union[A, object]] - - class ModelA(BaseModel): - type: Literal["modelA"] - - data: int - - class ModelB(BaseModel): - type: Literal["modelB"] - - required: str - - data: Union[A, B] - - # when constructing ModelA | ModelB, value data doesn't match ModelB exactly - missing `required` - m = construct_type( - value={"type": "modelB", "data": {"type": "a", "data": True}}, - type_=cast(Any, Annotated[Union[ModelA, ModelB], PropertyInfo(discriminator="type")]), - ) - - assert isinstance(m, ModelB) - - -def test_nested_discriminated_union() -> None: - class InnerType1(BaseModel): - type: Literal["type_1"] - - class InnerModel(BaseModel): - inner_value: str - - class InnerType2(BaseModel): - type: Literal["type_2"] - some_inner_model: InnerModel - - class Type1(BaseModel): - base_type: Literal["base_type_1"] - value: Annotated[ - Union[ - InnerType1, - InnerType2, - ], - PropertyInfo(discriminator="type"), - ] - - class Type2(BaseModel): - base_type: Literal["base_type_2"] - - T = Annotated[ - Union[ - Type1, - Type2, - ], - PropertyInfo(discriminator="base_type"), - ] - - model = construct_type( - type_=T, - value={ - "base_type": "base_type_1", - "value": { - "type": "type_2", - }, - }, - ) - assert isinstance(model, Type1) - assert isinstance(model.value, InnerType2) - - -@pytest.mark.skipif(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" diff --git a/tests/test_qs.py b/tests/test_qs.py deleted file mode 100644 index b47f757..0000000 --- a/tests/test_qs.py +++ /dev/null @@ -1,78 +0,0 @@ -from typing import Any, cast -from functools import partial -from urllib.parse import unquote - -import pytest - -from browser_use_sdk._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 deleted file mode 100644 index eb5821a..0000000 --- a/tests/test_required_args.py +++ /dev/null @@ -1,111 +0,0 @@ -from __future__ import annotations - -import pytest - -from browser_use_sdk._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 deleted file mode 100644 index c64b9ed..0000000 --- a/tests/test_response.py +++ /dev/null @@ -1,277 +0,0 @@ -import json -from typing import Any, List, Union, cast -from typing_extensions import Annotated - -import httpx -import pytest -import pydantic - -from browser_use_sdk import BaseModel, BrowserUse, AsyncBrowserUse -from browser_use_sdk._response import ( - APIResponse, - BaseAPIResponse, - AsyncAPIResponse, - BinaryAPIResponse, - AsyncBinaryAPIResponse, - extract_response_type, -) -from browser_use_sdk._streaming import Stream -from browser_use_sdk._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: BrowserUse) -> 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 browser_use_sdk import BaseModel`", - ): - response.parse(to=PydanticModel) - - -@pytest.mark.asyncio -async def test_async_response_parse_mismatched_basemodel(async_client: AsyncBrowserUse) -> 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 browser_use_sdk import BaseModel`", - ): - await response.parse(to=PydanticModel) - - -def test_response_parse_custom_stream(client: BrowserUse) -> 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: AsyncBrowserUse) -> 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: BrowserUse) -> 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: AsyncBrowserUse) -> 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: BrowserUse) -> 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: AsyncBrowserUse) -> 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: BrowserUse, 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: AsyncBrowserUse, 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: BrowserUse) -> 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: AsyncBrowserUse) -> 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 deleted file mode 100644 index 84f2452..0000000 --- a/tests/test_streaming.py +++ /dev/null @@ -1,250 +0,0 @@ -from __future__ import annotations - -from typing import Iterator, AsyncIterator - -import httpx -import pytest - -from browser_use_sdk import BrowserUse, AsyncBrowserUse -from browser_use_sdk._streaming import Stream, AsyncStream, ServerSentEvent - - -@pytest.mark.asyncio -@pytest.mark.parametrize("sync", [True, False], ids=["sync", "async"]) -async def test_basic(sync: bool, client: BrowserUse, async_client: AsyncBrowserUse) -> 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: BrowserUse, async_client: AsyncBrowserUse) -> 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: BrowserUse, async_client: AsyncBrowserUse) -> 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: BrowserUse, async_client: AsyncBrowserUse) -> 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: BrowserUse, async_client: AsyncBrowserUse) -> 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: BrowserUse, async_client: AsyncBrowserUse -) -> 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: BrowserUse, async_client: AsyncBrowserUse) -> 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: BrowserUse, async_client: AsyncBrowserUse) -> 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: BrowserUse, - async_client: AsyncBrowserUse, -) -> 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: BrowserUse, - async_client: AsyncBrowserUse, -) -> 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: BrowserUse, - async_client: AsyncBrowserUse, -) -> 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 deleted file mode 100644 index b336597..0000000 --- a/tests/test_transform.py +++ /dev/null @@ -1,453 +0,0 @@ -from __future__ import annotations - -import io -import pathlib -from typing import Any, Dict, List, Union, TypeVar, Iterable, Optional, cast -from datetime import date, datetime -from typing_extensions import Required, Annotated, TypedDict - -import pytest - -from browser_use_sdk._types import NOT_GIVEN, Base64FileInput -from browser_use_sdk._utils import ( - PropertyInfo, - transform as _transform, - parse_datetime, - async_transform as _async_transform, -) -from browser_use_sdk._compat import PYDANTIC_V2 -from browser_use_sdk._models import BaseModel - -_T = TypeVar("_T") - -SAMPLE_FILE_PATH = pathlib.Path(__file__).parent.joinpath("sample_file.txt") - - -async def transform( - data: _T, - expected_type: object, - use_async: bool, -) -> _T: - if use_async: - return await _async_transform(data, expected_type=expected_type) - - return _transform(data, expected_type=expected_type) - - -parametrize = pytest.mark.parametrize("use_async", [False, True], ids=["sync", "async"]) - - -class Foo1(TypedDict): - foo_bar: Annotated[str, PropertyInfo(alias="fooBar")] - - -@parametrize -@pytest.mark.asyncio -async def test_top_level_alias(use_async: bool) -> None: - assert await transform({"foo_bar": "hello"}, expected_type=Foo1, use_async=use_async) == {"fooBar": "hello"} - - -class Foo2(TypedDict): - bar: Bar2 - - -class Bar2(TypedDict): - this_thing: Annotated[int, PropertyInfo(alias="this__thing")] - baz: Annotated[Baz2, PropertyInfo(alias="Baz")] - - -class Baz2(TypedDict): - my_baz: Annotated[str, PropertyInfo(alias="myBaz")] - - -@parametrize -@pytest.mark.asyncio -async def test_recursive_typeddict(use_async: bool) -> None: - assert await transform({"bar": {"this_thing": 1}}, Foo2, use_async) == {"bar": {"this__thing": 1}} - assert await transform({"bar": {"baz": {"my_baz": "foo"}}}, Foo2, use_async) == {"bar": {"Baz": {"myBaz": "foo"}}} - - -class Foo3(TypedDict): - things: List[Bar3] - - -class Bar3(TypedDict): - my_field: Annotated[str, PropertyInfo(alias="myField")] - - -@parametrize -@pytest.mark.asyncio -async def test_list_of_typeddict(use_async: bool) -> None: - result = await transform({"things": [{"my_field": "foo"}, {"my_field": "foo2"}]}, Foo3, use_async) - assert result == {"things": [{"myField": "foo"}, {"myField": "foo2"}]} - - -class Foo4(TypedDict): - foo: Union[Bar4, Baz4] - - -class Bar4(TypedDict): - foo_bar: Annotated[str, PropertyInfo(alias="fooBar")] - - -class Baz4(TypedDict): - foo_baz: Annotated[str, PropertyInfo(alias="fooBaz")] - - -@parametrize -@pytest.mark.asyncio -async def test_union_of_typeddict(use_async: bool) -> None: - assert await transform({"foo": {"foo_bar": "bar"}}, Foo4, use_async) == {"foo": {"fooBar": "bar"}} - assert await transform({"foo": {"foo_baz": "baz"}}, Foo4, use_async) == {"foo": {"fooBaz": "baz"}} - assert await transform({"foo": {"foo_baz": "baz", "foo_bar": "bar"}}, Foo4, use_async) == { - "foo": {"fooBaz": "baz", "fooBar": "bar"} - } - - -class Foo5(TypedDict): - foo: Annotated[Union[Bar4, List[Baz4]], PropertyInfo(alias="FOO")] - - -class Bar5(TypedDict): - foo_bar: Annotated[str, PropertyInfo(alias="fooBar")] - - -class Baz5(TypedDict): - foo_baz: Annotated[str, PropertyInfo(alias="fooBaz")] - - -@parametrize -@pytest.mark.asyncio -async def test_union_of_list(use_async: bool) -> None: - assert await transform({"foo": {"foo_bar": "bar"}}, Foo5, use_async) == {"FOO": {"fooBar": "bar"}} - assert await transform( - { - "foo": [ - {"foo_baz": "baz"}, - {"foo_baz": "baz"}, - ] - }, - Foo5, - use_async, - ) == {"FOO": [{"fooBaz": "baz"}, {"fooBaz": "baz"}]} - - -class Foo6(TypedDict): - bar: Annotated[str, PropertyInfo(alias="Bar")] - - -@parametrize -@pytest.mark.asyncio -async def test_includes_unknown_keys(use_async: bool) -> None: - assert await transform({"bar": "bar", "baz_": {"FOO": 1}}, Foo6, use_async) == { - "Bar": "bar", - "baz_": {"FOO": 1}, - } - - -class Foo7(TypedDict): - bar: Annotated[List[Bar7], PropertyInfo(alias="bAr")] - foo: Bar7 - - -class Bar7(TypedDict): - foo: str - - -@parametrize -@pytest.mark.asyncio -async def test_ignores_invalid_input(use_async: bool) -> None: - assert await transform({"bar": ""}, Foo7, use_async) == {"bAr": ""} - assert await transform({"foo": ""}, Foo7, use_async) == {"foo": ""} - - -class DatetimeDict(TypedDict, total=False): - foo: Annotated[datetime, PropertyInfo(format="iso8601")] - - bar: Annotated[Optional[datetime], PropertyInfo(format="iso8601")] - - required: Required[Annotated[Optional[datetime], PropertyInfo(format="iso8601")]] - - list_: Required[Annotated[Optional[List[datetime]], PropertyInfo(format="iso8601")]] - - union: Annotated[Union[int, datetime], PropertyInfo(format="iso8601")] - - -class DateDict(TypedDict, total=False): - foo: Annotated[date, PropertyInfo(format="iso8601")] - - -class DatetimeModel(BaseModel): - foo: datetime - - -class DateModel(BaseModel): - foo: Optional[date] - - -@parametrize -@pytest.mark.asyncio -async def test_iso8601_format(use_async: bool) -> None: - dt = datetime.fromisoformat("2023-02-23T14:16:36.337692+00:00") - tz = "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 -@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"}] - } - - -@parametrize -@pytest.mark.asyncio -async def test_dictionary_items(use_async: bool) -> None: - class DictItems(TypedDict): - foo_baz: Annotated[str, PropertyInfo(alias="fooBaz")] - - assert await transform({"foo": {"foo_baz": "bar"}}, Dict[str, DictItems], use_async) == {"foo": {"fooBaz": "bar"}} - - -class TypedDictIterableUnionStr(TypedDict): - foo: Annotated[Union[str, Iterable[Baz8]], PropertyInfo(alias="FOO")] - - -@parametrize -@pytest.mark.asyncio -async def test_iterable_union_str(use_async: bool) -> None: - assert await transform({"foo": "bar"}, TypedDictIterableUnionStr, use_async) == {"FOO": "bar"} - assert cast(Any, await transform(iter([{"foo_baz": "bar"}]), Union[str, Iterable[Baz8]], use_async)) == [ - {"fooBaz": "bar"} - ] - - -class TypedDictBase64Input(TypedDict): - foo: Annotated[Union[str, Base64FileInput], PropertyInfo(format="base64")] - - -@parametrize -@pytest.mark.asyncio -async def test_base64_file_input(use_async: bool) -> None: - # strings are left as-is - assert await transform({"foo": "bar"}, TypedDictBase64Input, use_async) == {"foo": "bar"} - - # pathlib.Path is automatically converted to base64 - assert await transform({"foo": SAMPLE_FILE_PATH}, TypedDictBase64Input, use_async) == { - "foo": "SGVsbG8sIHdvcmxkIQo=" - } # type: ignore[comparison-overlap] - - # io instances are automatically converted to base64 - assert await transform({"foo": io.StringIO("Hello, world!")}, TypedDictBase64Input, use_async) == { - "foo": "SGVsbG8sIHdvcmxkIQ==" - } # type: ignore[comparison-overlap] - assert await transform({"foo": io.BytesIO(b"Hello, world!")}, TypedDictBase64Input, use_async) == { - "foo": "SGVsbG8sIHdvcmxkIQ==" - } # type: ignore[comparison-overlap] - - -@parametrize -@pytest.mark.asyncio -async def test_transform_skipping(use_async: bool) -> None: - # lists of ints are left as-is - data = [1, 2, 3] - assert await transform(data, List[int], use_async) is data - - # iterables of ints are converted to a list - data = iter([1, 2, 3]) - assert await transform(data, Iterable[int], use_async) == [1, 2, 3] - - -@parametrize -@pytest.mark.asyncio -async def test_strips_notgiven(use_async: bool) -> None: - assert await transform({"foo_bar": "bar"}, Foo1, use_async) == {"fooBar": "bar"} - assert await transform({"foo_bar": NOT_GIVEN}, Foo1, use_async) == {} diff --git a/tests/test_utils/test_proxy.py b/tests/test_utils/test_proxy.py deleted file mode 100644 index a76d3a9..0000000 --- a/tests/test_utils/test_proxy.py +++ /dev/null @@ -1,34 +0,0 @@ -import operator -from typing import Any -from typing_extensions import override - -from browser_use_sdk._utils import LazyProxy - - -class RecursiveLazyProxy(LazyProxy[Any]): - @override - def __load__(self) -> Any: - return self - - def __call__(self, *_args: Any, **_kwds: Any) -> Any: - raise RuntimeError("This should never be called!") - - -def test_recursive_proxy() -> None: - proxy = RecursiveLazyProxy() - assert repr(proxy) == "RecursiveLazyProxy" - assert str(proxy) == "RecursiveLazyProxy" - assert dir(proxy) == [] - assert type(proxy).__name__ == "RecursiveLazyProxy" - assert type(operator.attrgetter("name.foo.bar.baz")(proxy)).__name__ == "RecursiveLazyProxy" - - -def test_isinstance_does_not_error() -> None: - class AlwaysErrorProxy(LazyProxy[Any]): - @override - def __load__(self) -> Any: - raise RuntimeError("Mocking missing dependency") - - proxy = AlwaysErrorProxy() - assert not isinstance(proxy, dict) - assert isinstance(proxy, LazyProxy) diff --git a/tests/test_utils/test_typing.py b/tests/test_utils/test_typing.py deleted file mode 100644 index e12cde1..0000000 --- a/tests/test_utils/test_typing.py +++ /dev/null @@ -1,73 +0,0 @@ -from __future__ import annotations - -from typing import Generic, TypeVar, cast - -from browser_use_sdk._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/test_webhooks.py b/tests/test_webhooks.py deleted file mode 100644 index 84f09d7..0000000 --- a/tests/test_webhooks.py +++ /dev/null @@ -1,151 +0,0 @@ -"""Tests for webhook functionality.""" - -from __future__ import annotations - -from typing import Any, Dict -from datetime import datetime, timezone - -import pytest - -from browser_use_sdk._compat import PYDANTIC_V2 -from browser_use_sdk.lib.webhooks import ( - WebhookTest, - WebhookTestPayload, - create_webhook_signature, - verify_webhook_event_signature, -) - -# Signature Creation --------------------------------------------------------- - - -@pytest.mark.skipif(not PYDANTIC_V2, reason="Webhook tests only run in Pydantic V2") -def test_create_webhook_signature() -> None: - """Test webhook signature creation.""" - secret = "test-secret-key" - timestamp = "2023-01-01T00:00:00Z" - payload = {"test": "ok"} - - signature = create_webhook_signature(payload, timestamp, secret) - - assert isinstance(signature, str) - assert len(signature) == 64 - - signature2 = create_webhook_signature(payload, timestamp, secret) - assert signature == signature2 - - different_payload = {"test": "different"} - different_signature = create_webhook_signature(different_payload, timestamp, secret) - - assert signature != different_signature - - -# Webhook Verification -------------------------------------------------------- - - -@pytest.mark.skipif(not PYDANTIC_V2, reason="Webhook tests only run in Pydantic V2") -def test_verify_webhook_event_signature_valid() -> None: - """Test webhook signature verification with valid signature.""" - secret = "test-secret-key" - timestamp = "2023-01-01T00:00:00Z" - - payload = WebhookTestPayload(test="ok") - webhook = WebhookTest(type="test", timestamp=datetime.now(timezone.utc), payload=payload) - signature = create_webhook_signature(webhook.payload.model_dump(), timestamp, secret) - - # Verify signature - verified_webhook = verify_webhook_event_signature( - body=webhook.model_dump(), - secret=secret, - timestamp=timestamp, - expected_signature=signature, - ) - - assert verified_webhook is not None - assert isinstance(verified_webhook, WebhookTest) - assert verified_webhook.payload.test == "ok" - - -@pytest.mark.skipif(not PYDANTIC_V2, reason="Webhook tests only run in Pydantic V2") -def test_verify_webhook_event_signature_invalid_signature() -> None: - """Test webhook signature verification with invalid signature.""" - secret = "test-secret-key" - timestamp = "2023-01-01T00:00:00Z" - - # Create test webhook - payload = WebhookTestPayload(test="ok") - webhook = WebhookTest(type="test", timestamp=datetime.now(timezone.utc), payload=payload) - - verified_webhook = verify_webhook_event_signature( - body=webhook.model_dump(), - secret=secret, - timestamp=timestamp, - expected_signature="random_invalid_signature", - ) - - assert verified_webhook is None - - -@pytest.mark.skipif(not PYDANTIC_V2, reason="Webhook tests only run in Pydantic V2") -def test_verify_webhook_event_signature_wrong_secret() -> None: - """Test webhook signature verification with wrong secret.""" - - timestamp = "2023-01-01T00:00:00Z" - - # Create test webhook - payload = WebhookTestPayload(test="ok") - webhook = WebhookTest(type="test", timestamp=datetime.now(timezone.utc), payload=payload) - - # Create signature with correct secret - signature = create_webhook_signature( - payload=payload.model_dump(), - timestamp=timestamp, - secret="test-secret-key", - ) - - # Verify with wrong secret - verified_webhook = verify_webhook_event_signature( - body=webhook.model_dump(), - secret="wrong-secret-key", - timestamp=timestamp, - expected_signature=signature, - ) - - assert verified_webhook is None - - -@pytest.mark.skipif(not PYDANTIC_V2, reason="Webhook tests only run in Pydantic V2") -def test_verify_webhook_event_signature_string_body() -> None: - """Test webhook signature verification with string body.""" - secret = "test-secret-key" - timestamp = "2023-01-01T00:00:00Z" - - # Create test webhook - payload = WebhookTestPayload(test="ok") - webhook = WebhookTest(type="test", timestamp=datetime.now(timezone.utc), payload=payload) - signature = create_webhook_signature(webhook.payload.model_dump(), timestamp, secret) - - verified_webhook = verify_webhook_event_signature( - body=webhook.model_dump_json(), - secret=secret, - timestamp=timestamp, - expected_signature=signature, - ) - - assert verified_webhook is not None - assert isinstance(verified_webhook, WebhookTest) - - -@pytest.mark.skipif(not PYDANTIC_V2, reason="Webhook tests only run in Pydantic V2") -def test_verify_webhook_event_signature_invalid_body() -> None: - """Test webhook signature verification with invalid body.""" - secret = "test-secret-key" - timestamp = "2023-01-01T00:00:00Z" - - # Invalid webhook data - invalid_body: Dict[str, Any] = {"type": "invalid_type", "timestamp": "invalid", "payload": {}} - - verified_webhook = verify_webhook_event_signature( - body=invalid_body, secret=secret, expected_signature="some_signature", timestamp=timestamp - ) - - assert verified_webhook is None diff --git a/tests/utils.py b/tests/utils.py deleted file mode 100644 index 858cc96..0000000 --- a/tests/utils.py +++ /dev/null @@ -1,159 +0,0 @@ -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 browser_use_sdk._types import Omit, NoneType -from browser_use_sdk._utils import ( - is_dict, - is_list, - is_list_type, - is_union_type, - extract_type_arg, - is_annotated_type, - is_type_alias_type, -) -from browser_use_sdk._compat import PYDANTIC_V2, field_outer_type, get_model_fields -from browser_use_sdk._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: - if is_type_alias_type(type_): - type_ = type_.__value__ - - # unwrap `Annotated[T, ...]` -> `T` - if is_annotated_type(type_): - type_ = extract_type_arg(type_, 0) - - if allow_none and value is None: - return - - if type_ is None or type_ is NoneType: - assert value is None - return - - origin = get_origin(type_) or type_ - - if is_list_type(type_): - return _assert_list_type(type_, value) - - if 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) diff --git a/tests/utils/__init__.py b/tests/utils/__init__.py new file mode 100644 index 0000000..f3ea265 --- /dev/null +++ b/tests/utils/__init__.py @@ -0,0 +1,2 @@ +# This file was auto-generated by Fern from our API Definition. + diff --git a/tests/utils/assets/models/__init__.py b/tests/utils/assets/models/__init__.py new file mode 100644 index 0000000..2cf0126 --- /dev/null +++ b/tests/utils/assets/models/__init__.py @@ -0,0 +1,21 @@ +# This file was auto-generated by Fern from our API Definition. + +# This file was auto-generated by Fern from our API Definition. + +from .circle import CircleParams +from .object_with_defaults import ObjectWithDefaultsParams +from .object_with_optional_field import ObjectWithOptionalFieldParams +from .shape import Shape_CircleParams, Shape_SquareParams, ShapeParams +from .square import SquareParams +from .undiscriminated_shape import UndiscriminatedShapeParams + +__all__ = [ + "CircleParams", + "ObjectWithDefaultsParams", + "ObjectWithOptionalFieldParams", + "ShapeParams", + "Shape_CircleParams", + "Shape_SquareParams", + "SquareParams", + "UndiscriminatedShapeParams", +] diff --git a/tests/utils/assets/models/circle.py b/tests/utils/assets/models/circle.py new file mode 100644 index 0000000..e2a7d91 --- /dev/null +++ b/tests/utils/assets/models/circle.py @@ -0,0 +1,11 @@ +# This file was auto-generated by Fern from our API Definition. + +# This file was auto-generated by Fern from our API Definition. + +import typing_extensions + +from browser_use.core.serialization import FieldMetadata + + +class CircleParams(typing_extensions.TypedDict): + radius_measurement: typing_extensions.Annotated[float, FieldMetadata(alias="radiusMeasurement")] diff --git a/tests/utils/assets/models/color.py b/tests/utils/assets/models/color.py new file mode 100644 index 0000000..2aa2c4c --- /dev/null +++ b/tests/utils/assets/models/color.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +# This file was auto-generated by Fern from our API Definition. + +import typing + +Color = typing.Union[typing.Literal["red", "blue"], typing.Any] diff --git a/tests/utils/assets/models/object_with_defaults.py b/tests/utils/assets/models/object_with_defaults.py new file mode 100644 index 0000000..a977b1d --- /dev/null +++ b/tests/utils/assets/models/object_with_defaults.py @@ -0,0 +1,15 @@ +# This file was auto-generated by Fern from our API Definition. + +# This file was auto-generated by Fern from our API Definition. + +import typing_extensions + + +class ObjectWithDefaultsParams(typing_extensions.TypedDict): + """ + Defines properties with default values and validation rules. + """ + + decimal: typing_extensions.NotRequired[float] + string: typing_extensions.NotRequired[str] + required_string: str diff --git a/tests/utils/assets/models/object_with_optional_field.py b/tests/utils/assets/models/object_with_optional_field.py new file mode 100644 index 0000000..eac76e0 --- /dev/null +++ b/tests/utils/assets/models/object_with_optional_field.py @@ -0,0 +1,35 @@ +# This file was auto-generated by Fern from our API Definition. + +# This file was auto-generated by Fern from our API Definition. + +import datetime as dt +import typing +import uuid + +import typing_extensions +from .color import Color +from .shape import ShapeParams +from .undiscriminated_shape import UndiscriminatedShapeParams + +from browser_use.core.serialization import FieldMetadata + + +class ObjectWithOptionalFieldParams(typing_extensions.TypedDict): + literal: typing.Literal["lit_one"] + string: typing_extensions.NotRequired[str] + integer: typing_extensions.NotRequired[int] + long_: typing_extensions.NotRequired[typing_extensions.Annotated[int, FieldMetadata(alias="long")]] + double: typing_extensions.NotRequired[float] + bool_: typing_extensions.NotRequired[typing_extensions.Annotated[bool, FieldMetadata(alias="bool")]] + datetime: typing_extensions.NotRequired[dt.datetime] + date: typing_extensions.NotRequired[dt.date] + uuid_: typing_extensions.NotRequired[typing_extensions.Annotated[uuid.UUID, FieldMetadata(alias="uuid")]] + base_64: typing_extensions.NotRequired[typing_extensions.Annotated[str, FieldMetadata(alias="base64")]] + list_: typing_extensions.NotRequired[typing_extensions.Annotated[typing.Sequence[str], FieldMetadata(alias="list")]] + set_: typing_extensions.NotRequired[typing_extensions.Annotated[typing.Set[str], FieldMetadata(alias="set")]] + map_: typing_extensions.NotRequired[typing_extensions.Annotated[typing.Dict[int, str], FieldMetadata(alias="map")]] + enum: typing_extensions.NotRequired[Color] + union: typing_extensions.NotRequired[ShapeParams] + second_union: typing_extensions.NotRequired[ShapeParams] + undiscriminated_union: typing_extensions.NotRequired[UndiscriminatedShapeParams] + any: typing.Optional[typing.Any] diff --git a/tests/utils/assets/models/shape.py b/tests/utils/assets/models/shape.py new file mode 100644 index 0000000..1c7b3cb --- /dev/null +++ b/tests/utils/assets/models/shape.py @@ -0,0 +1,28 @@ +# This file was auto-generated by Fern from our API Definition. + +# This file was auto-generated by Fern from our API Definition. + +from __future__ import annotations + +import typing + +import typing_extensions + +from browser_use.core.serialization import FieldMetadata + + +class Base(typing_extensions.TypedDict): + id: str + + +class Shape_CircleParams(Base): + shape_type: typing_extensions.Annotated[typing.Literal["circle"], FieldMetadata(alias="shapeType")] + radius_measurement: typing_extensions.Annotated[float, FieldMetadata(alias="radiusMeasurement")] + + +class Shape_SquareParams(Base): + shape_type: typing_extensions.Annotated[typing.Literal["square"], FieldMetadata(alias="shapeType")] + length_measurement: typing_extensions.Annotated[float, FieldMetadata(alias="lengthMeasurement")] + + +ShapeParams = typing.Union[Shape_CircleParams, Shape_SquareParams] diff --git a/tests/utils/assets/models/square.py b/tests/utils/assets/models/square.py new file mode 100644 index 0000000..3021cd4 --- /dev/null +++ b/tests/utils/assets/models/square.py @@ -0,0 +1,11 @@ +# This file was auto-generated by Fern from our API Definition. + +# This file was auto-generated by Fern from our API Definition. + +import typing_extensions + +from browser_use.core.serialization import FieldMetadata + + +class SquareParams(typing_extensions.TypedDict): + length_measurement: typing_extensions.Annotated[float, FieldMetadata(alias="lengthMeasurement")] diff --git a/tests/utils/assets/models/undiscriminated_shape.py b/tests/utils/assets/models/undiscriminated_shape.py new file mode 100644 index 0000000..99f12b3 --- /dev/null +++ b/tests/utils/assets/models/undiscriminated_shape.py @@ -0,0 +1,10 @@ +# This file was auto-generated by Fern from our API Definition. + +# This file was auto-generated by Fern from our API Definition. + +import typing + +from .circle import CircleParams +from .square import SquareParams + +UndiscriminatedShapeParams = typing.Union[CircleParams, SquareParams] diff --git a/tests/utils/test_http_client.py b/tests/utils/test_http_client.py new file mode 100644 index 0000000..d9cdaaa --- /dev/null +++ b/tests/utils/test_http_client.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from browser_use.core.http_client import get_request_body +from browser_use.core.request_options import RequestOptions + + +def get_request_options() -> RequestOptions: + return {"additional_body_parameters": {"see you": "later"}} + + +def test_get_json_request_body() -> None: + json_body, data_body = get_request_body(json={"hello": "world"}, data=None, request_options=None, omit=None) + assert json_body == {"hello": "world"} + assert data_body is None + + json_body_extras, data_body_extras = get_request_body( + json={"goodbye": "world"}, data=None, request_options=get_request_options(), omit=None + ) + + assert json_body_extras == {"goodbye": "world", "see you": "later"} + assert data_body_extras is None + + +def test_get_files_request_body() -> None: + json_body, data_body = get_request_body(json=None, data={"hello": "world"}, request_options=None, omit=None) + assert data_body == {"hello": "world"} + assert json_body is None + + json_body_extras, data_body_extras = get_request_body( + json=None, data={"goodbye": "world"}, request_options=get_request_options(), omit=None + ) + + assert data_body_extras == {"goodbye": "world", "see you": "later"} + assert json_body_extras is None + + +def test_get_none_request_body() -> None: + json_body, data_body = get_request_body(json=None, data=None, request_options=None, omit=None) + assert data_body is None + assert json_body is None + + json_body_extras, data_body_extras = get_request_body( + json=None, data=None, request_options=get_request_options(), omit=None + ) + + assert json_body_extras == {"see you": "later"} + assert data_body_extras is None + + +def test_get_empty_json_request_body() -> None: + unrelated_request_options: RequestOptions = {"max_retries": 3} + json_body, data_body = get_request_body(json=None, data=None, request_options=unrelated_request_options, omit=None) + assert json_body is None + assert data_body is None + + json_body_extras, data_body_extras = get_request_body( + json={}, data=None, request_options=unrelated_request_options, omit=None + ) + + assert json_body_extras is None + assert data_body_extras is None diff --git a/tests/utils/test_query_encoding.py b/tests/utils/test_query_encoding.py new file mode 100644 index 0000000..aebf381 --- /dev/null +++ b/tests/utils/test_query_encoding.py @@ -0,0 +1,37 @@ +# This file was auto-generated by Fern from our API Definition. + + +from browser_use.core.query_encoder import encode_query + + +def test_query_encoding_deep_objects() -> None: + assert encode_query({"hello world": "hello world"}) == [("hello world", "hello world")] + assert encode_query({"hello_world": {"hello": "world"}}) == [("hello_world[hello]", "world")] + assert encode_query({"hello_world": {"hello": {"world": "today"}, "test": "this"}, "hi": "there"}) == [ + ("hello_world[hello][world]", "today"), + ("hello_world[test]", "this"), + ("hi", "there"), + ] + + +def test_query_encoding_deep_object_arrays() -> None: + assert encode_query({"objects": [{"key": "hello", "value": "world"}, {"key": "foo", "value": "bar"}]}) == [ + ("objects[key]", "hello"), + ("objects[value]", "world"), + ("objects[key]", "foo"), + ("objects[value]", "bar"), + ] + assert encode_query( + {"users": [{"name": "string", "tags": ["string"]}, {"name": "string2", "tags": ["string2", "string3"]}]} + ) == [ + ("users[name]", "string"), + ("users[tags]", "string"), + ("users[name]", "string2"), + ("users[tags]", "string2"), + ("users[tags]", "string3"), + ] + + +def test_encode_query_with_none() -> None: + encoded = encode_query(None) + assert encoded is None diff --git a/tests/utils/test_serialization.py b/tests/utils/test_serialization.py new file mode 100644 index 0000000..1c91638 --- /dev/null +++ b/tests/utils/test_serialization.py @@ -0,0 +1,72 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import Any, List + +from .assets.models import ObjectWithOptionalFieldParams, ShapeParams + +from browser_use.core.serialization import convert_and_respect_annotation_metadata + +UNION_TEST: ShapeParams = {"radius_measurement": 1.0, "shape_type": "circle", "id": "1"} +UNION_TEST_CONVERTED = {"shapeType": "circle", "radiusMeasurement": 1.0, "id": "1"} + + +def test_convert_and_respect_annotation_metadata() -> None: + data: ObjectWithOptionalFieldParams = { + "string": "string", + "long_": 12345, + "bool_": True, + "literal": "lit_one", + "any": "any", + } + converted = convert_and_respect_annotation_metadata( + object_=data, annotation=ObjectWithOptionalFieldParams, direction="write" + ) + assert converted == {"string": "string", "long": 12345, "bool": True, "literal": "lit_one", "any": "any"} + + +def test_convert_and_respect_annotation_metadata_in_list() -> None: + data: List[ObjectWithOptionalFieldParams] = [ + {"string": "string", "long_": 12345, "bool_": True, "literal": "lit_one", "any": "any"}, + {"string": "another string", "long_": 67890, "list_": [], "literal": "lit_one", "any": "any"}, + ] + converted = convert_and_respect_annotation_metadata( + object_=data, annotation=List[ObjectWithOptionalFieldParams], direction="write" + ) + + assert converted == [ + {"string": "string", "long": 12345, "bool": True, "literal": "lit_one", "any": "any"}, + {"string": "another string", "long": 67890, "list": [], "literal": "lit_one", "any": "any"}, + ] + + +def test_convert_and_respect_annotation_metadata_in_nested_object() -> None: + data: ObjectWithOptionalFieldParams = { + "string": "string", + "long_": 12345, + "union": UNION_TEST, + "literal": "lit_one", + "any": "any", + } + converted = convert_and_respect_annotation_metadata( + object_=data, annotation=ObjectWithOptionalFieldParams, direction="write" + ) + + assert converted == { + "string": "string", + "long": 12345, + "union": UNION_TEST_CONVERTED, + "literal": "lit_one", + "any": "any", + } + + +def test_convert_and_respect_annotation_metadata_in_union() -> None: + converted = convert_and_respect_annotation_metadata(object_=UNION_TEST, annotation=ShapeParams, direction="write") + + assert converted == UNION_TEST_CONVERTED + + +def test_convert_and_respect_annotation_metadata_with_empty_object() -> None: + data: Any = {} + converted = convert_and_respect_annotation_metadata(object_=data, annotation=ShapeParams, direction="write") + assert converted == data